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:
|
spec:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
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:
|
permissions:
|
||||||
type: array
|
type: array
|
||||||
description: >-
|
description: >-
|
||||||
|
|
@ -44,7 +53,7 @@ spec:
|
||||||
- reset
|
- reset
|
||||||
- accessConsole
|
- accessConsole
|
||||||
- "*"
|
- "*"
|
||||||
default: []
|
default: ["accessConsole"]
|
||||||
required:
|
required:
|
||||||
- permissions
|
- permissions
|
||||||
# either Namespaced or Cluster
|
# either Namespaced or Cluster
|
||||||
|
|
|
||||||
|
|
@ -994,6 +994,10 @@ spec:
|
||||||
type: array
|
type: array
|
||||||
description: >-
|
description: >-
|
||||||
Defines permissions for accessing and manipulating the VM.
|
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:
|
items:
|
||||||
type: object
|
type: object
|
||||||
description: >-
|
description: >-
|
||||||
|
|
@ -1017,12 +1021,13 @@ spec:
|
||||||
- stop
|
- stop
|
||||||
- reset
|
- reset
|
||||||
- accessConsole
|
- accessConsole
|
||||||
|
- takeConsole
|
||||||
- "*"
|
- "*"
|
||||||
default: []
|
default: []
|
||||||
pools:
|
pools:
|
||||||
type: array
|
type: array
|
||||||
description: >-
|
description: >-
|
||||||
List of pools to which this VM belongs.
|
List of pools this VM belongs to.
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
default: []
|
default: []
|
||||||
|
|
@ -1486,6 +1491,24 @@ spec:
|
||||||
by the runner if password protection is not enabled.
|
by the runner if password protection is not enabled.
|
||||||
type: integer
|
type: integer
|
||||||
default: 0
|
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:
|
conditions:
|
||||||
description: >-
|
description: >-
|
||||||
List of component conditions observed
|
List of component conditions observed
|
||||||
|
|
|
||||||
|
|
@ -40,15 +40,30 @@
|
||||||
- name: admin
|
- name: admin
|
||||||
fullName: Administrator
|
fullName: Administrator
|
||||||
password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21."
|
password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21."
|
||||||
- name: test
|
- name: operator
|
||||||
fullName: Test Account
|
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"
|
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
|
||||||
"/RoleConfigurator":
|
"/RoleConfigurator":
|
||||||
rolesByUser:
|
rolesByUser:
|
||||||
# User admin has role admin
|
# User admin has role admin
|
||||||
admin:
|
admin:
|
||||||
- admin
|
- admin
|
||||||
test:
|
operator:
|
||||||
|
- operator
|
||||||
|
test1:
|
||||||
|
- user
|
||||||
|
test2:
|
||||||
|
- user
|
||||||
|
test3:
|
||||||
- user
|
- user
|
||||||
# All users have role other
|
# All users have role other
|
||||||
"*":
|
"*":
|
||||||
|
|
@ -59,8 +74,10 @@
|
||||||
# Admins can use all conlets
|
# Admins can use all conlets
|
||||||
admin:
|
admin:
|
||||||
- "*"
|
- "*"
|
||||||
|
operator:
|
||||||
|
- org.jdrupes.vmoperator.vmaccess.VmAccess
|
||||||
user:
|
user:
|
||||||
- org.jdrupes.vmoperator.vmviewer.VmViewer
|
- org.jdrupes.vmoperator.vmaccess.VmAccess
|
||||||
# Others cannot use any conlet (except login conlet to log out)
|
# Others cannot use any conlet (except login conlet to log out)
|
||||||
other:
|
other:
|
||||||
- org.jgrapes.webconlet.oidclogin.LoginConlet
|
- org.jgrapes.webconlet.oidclogin.LoginConlet
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,13 @@ patches:
|
||||||
- name: admin
|
- name: admin
|
||||||
fullName: Administrator
|
fullName: Administrator
|
||||||
password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21."
|
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
|
fullName: Test Account
|
||||||
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
|
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
|
||||||
"/RoleConfigurator":
|
"/RoleConfigurator":
|
||||||
|
|
@ -62,7 +68,11 @@ patches:
|
||||||
# User admin has role admin
|
# User admin has role admin
|
||||||
admin:
|
admin:
|
||||||
- admin
|
- admin
|
||||||
test:
|
test1:
|
||||||
|
- user
|
||||||
|
test2:
|
||||||
|
- user
|
||||||
|
test3:
|
||||||
- user
|
- user
|
||||||
# All users have role other
|
# All users have role other
|
||||||
"*":
|
"*":
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,11 @@ metadata:
|
||||||
namespace: vmop-dev
|
namespace: vmop-dev
|
||||||
name: test-vms
|
name: test-vms
|
||||||
spec:
|
spec:
|
||||||
|
retention: "PT1m"
|
||||||
permissions:
|
permissions:
|
||||||
- user: admin
|
- user: admin
|
||||||
may:
|
may:
|
||||||
- accessConsole
|
- accessConsole
|
||||||
|
- role: user
|
||||||
|
may:
|
||||||
|
- accessConsole
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,6 @@ spec:
|
||||||
- role: admin
|
- role: admin
|
||||||
may:
|
may:
|
||||||
- "*"
|
- "*"
|
||||||
- user: test
|
|
||||||
may:
|
|
||||||
- accessConsole
|
|
||||||
|
|
||||||
guestShutdownStops: true
|
guestShutdownStops: true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,6 @@ spec:
|
||||||
- user: admin
|
- user: admin
|
||||||
may:
|
may:
|
||||||
- "*"
|
- "*"
|
||||||
- user: test
|
|
||||||
may:
|
|
||||||
- "accessConsole"
|
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,5 @@ dependencies {
|
||||||
api 'org.jgrapes:org.jgrapes.core:[1.22.1,2)'
|
api 'org.jgrapes:org.jgrapes.core:[1.22.1,2)'
|
||||||
api 'io.kubernetes:client-java:[19.0.0,20.0.0)'
|
api 'io.kubernetes:client-java:[19.0.0,20.0.0)'
|
||||||
api 'org.yaml:snakeyaml'
|
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 <O> the generic type
|
||||||
* @param <L> the generic type
|
* @param <L> the generic type
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis",
|
||||||
|
"PMD.CouplingBetweenObjects" })
|
||||||
public class K8sClusterGenericStub<O extends KubernetesObject,
|
public class K8sClusterGenericStub<O extends KubernetesObject,
|
||||||
L extends KubernetesListObject> {
|
L extends KubernetesListObject> {
|
||||||
protected final K8sClient client;
|
protected final K8sClient client;
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,18 @@
|
||||||
|
|
||||||
package org.jdrupes.vmoperator.common;
|
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 io.kubernetes.client.openapi.models.V1ObjectMeta;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
@ -35,9 +40,12 @@ import org.jdrupes.vmoperator.util.DataPath;
|
||||||
/**
|
/**
|
||||||
* Represents a VM definition.
|
* Represents a VM definition.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({ "PMD.DataClass" })
|
@SuppressWarnings({ "PMD.DataClass", "PMD.TooManyMethods" })
|
||||||
public class VmDefinition {
|
public class VmDefinition {
|
||||||
|
|
||||||
|
private static ObjectMapper objectMapper
|
||||||
|
= new ObjectMapper().registerModule(new JavaTimeModule());
|
||||||
|
|
||||||
private String kind;
|
private String kind;
|
||||||
private String apiVersion;
|
private String apiVersion;
|
||||||
private V1ObjectMeta metadata;
|
private V1ObjectMeta metadata;
|
||||||
|
|
@ -57,7 +65,7 @@ public class VmDefinition {
|
||||||
*/
|
*/
|
||||||
public enum Permission {
|
public enum Permission {
|
||||||
START("start"), STOP("stop"), RESET("reset"),
|
START("start"), STOP("stop"), RESET("reset"),
|
||||||
ACCESS_CONSOLE("accessConsole");
|
ACCESS_CONSOLE("accessConsole"), TAKE_CONSOLE("takeConsole");
|
||||||
|
|
||||||
@SuppressWarnings("PMD.UseConcurrentHashMap")
|
@SuppressWarnings("PMD.UseConcurrentHashMap")
|
||||||
private static Map<String, Permission> reprs = new HashMap<>();
|
private static Map<String, Permission> reprs = new HashMap<>();
|
||||||
|
|
@ -88,12 +96,44 @@ public class VmDefinition {
|
||||||
return Set.of(reprs.get(value));
|
return Set.of(reprs.get(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To string.
|
||||||
|
*
|
||||||
|
* @return the string
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return repr;
|
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.
|
* Gets the kind.
|
||||||
*
|
*
|
||||||
|
|
@ -157,6 +197,16 @@ public class VmDefinition {
|
||||||
this.metadata = metadata;
|
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.
|
* Gets the spec.
|
||||||
*
|
*
|
||||||
|
|
@ -245,6 +295,82 @@ public class VmDefinition {
|
||||||
this.status = status;
|
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).
|
* Set extra data (locally used, unknown to kubernetes).
|
||||||
*
|
*
|
||||||
|
|
@ -260,6 +386,7 @@ public class VmDefinition {
|
||||||
/**
|
/**
|
||||||
* Return extra data.
|
* Return extra data.
|
||||||
*
|
*
|
||||||
|
* @param <T> the generic type
|
||||||
* @param property the property
|
* @param property the property
|
||||||
* @return the object
|
* @return the object
|
||||||
*/
|
*/
|
||||||
|
|
@ -287,12 +414,11 @@ public class VmDefinition {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the requested VM state
|
* Return the requested VM state.
|
||||||
*
|
*
|
||||||
* @return the string
|
* @return the string
|
||||||
*/
|
*/
|
||||||
public RequestedVmState vmState() {
|
public RequestedVmState vmState() {
|
||||||
// TODO
|
|
||||||
return fromVm("state")
|
return fromVm("state")
|
||||||
.map(s -> "Running".equals(s) ? RequestedVmState.RUNNING
|
.map(s -> "Running".equals(s) ? RequestedVmState.RUNNING
|
||||||
: RequestedVmState.STOPPED)
|
: RequestedVmState.STOPPED)
|
||||||
|
|
@ -329,4 +455,38 @@ public class VmDefinition {
|
||||||
return this.<Number> fromStatus("displayPasswordSerial")
|
return this.<Number> fromStatus("displayPasswordSerial")
|
||||||
.map(Number::longValue);
|
.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;
|
package org.jdrupes.vmoperator.common;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.EnumSet;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
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;
|
import org.jdrupes.vmoperator.util.DataPath;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -37,10 +38,21 @@ import org.jdrupes.vmoperator.util.DataPath;
|
||||||
public class VmPool {
|
public class VmPool {
|
||||||
|
|
||||||
private String name;
|
private String name;
|
||||||
|
private String retention;
|
||||||
|
private boolean defined;
|
||||||
private List<Grant> permissions = Collections.emptyList();
|
private List<Grant> permissions = Collections.emptyList();
|
||||||
private final Set<String> vms
|
private final Set<String> vms
|
||||||
= Collections.synchronizedSet(new HashSet<>());
|
= Collections.synchronizedSet(new HashSet<>());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new vm pool.
|
||||||
|
*
|
||||||
|
* @param name the name
|
||||||
|
*/
|
||||||
|
public VmPool(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the 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
|
* @return the permissions
|
||||||
*/
|
*/
|
||||||
public List<Grant> permissions() {
|
public List<Grant> permissions() {
|
||||||
|
|
@ -84,6 +134,11 @@ public class VmPool {
|
||||||
return vms;
|
return vms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To string.
|
||||||
|
*
|
||||||
|
* @return the string
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
|
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
|
||||||
public String toString() {
|
public String toString() {
|
||||||
|
|
@ -93,9 +148,8 @@ public class VmPool {
|
||||||
if (vms.size() <= 3) {
|
if (vms.size() <= 3) {
|
||||||
builder.append(vms);
|
builder.append(vms);
|
||||||
} else {
|
} else {
|
||||||
builder.append('[');
|
builder.append('[').append(vms.stream().limit(3).map(s -> s + ",")
|
||||||
vms.stream().limit(3).map(s -> s + ",").forEach(builder::append);
|
.collect(Collectors.joining())).append("...]");
|
||||||
builder.append("...]");
|
|
||||||
}
|
}
|
||||||
builder.append(']');
|
builder.append(']');
|
||||||
return builder.toString();
|
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 lastUsed the last used
|
||||||
* @param role the role
|
* @return the instant
|
||||||
* @param may the may
|
|
||||||
*/
|
*/
|
||||||
public record Grant(String user, String role, Set<Permission> may) {
|
public Instant retainUntil(Instant lastUsed) {
|
||||||
|
if (retention.startsWith("P")) {
|
||||||
@Override
|
return lastUsed.plus(Duration.parse(retention));
|
||||||
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 Instant.parse(retention);
|
||||||
return builder.toString();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 {
|
dependencies {
|
||||||
api project(':org.jdrupes.vmoperator.common')
|
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.io:[2.12.1,3)'
|
||||||
implementation 'org.jgrapes:org.jgrapes.http:[3.5.0,4)'
|
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.vuejs:[1.8.0,2)'
|
||||||
implementation 'org.jgrapes:org.jgrapes.webconsole.rbac:[1.4.0,2)'
|
implementation 'org.jgrapes:org.jgrapes.webconsole.rbac:[1.4.0,2)'
|
||||||
implementation 'org.jgrapes:org.jgrapes.webconlet.oidclogin:[1.7.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.
|
// Maybe override logging.properties from reconciler configuration.
|
||||||
DataPath.<String> get(model, "reconciler", "loggingProperties")
|
DataPath.<String> get(model, "reconciler", "loggingProperties")
|
||||||
.ifPresent(props -> {
|
.ifPresent(props -> {
|
||||||
GsonPtr.to(mapDef.getRaw()).get(JsonObject.class, "data")
|
GsonPtr.to(mapDef.getRaw()).getAs(JsonObject.class, "data")
|
||||||
.get().addProperty("logging.properties", props);
|
.get().addProperty("logging.properties", props);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Maybe override logging.properties from VM definition.
|
// Maybe override logging.properties from VM definition.
|
||||||
DataPath.<String> get(model, "cr", "spec", "loggingProperties")
|
DataPath.<String> get(model, "cr", "spec", "loggingProperties")
|
||||||
.ifPresent(props -> {
|
.ifPresent(props -> {
|
||||||
GsonPtr.to(mapDef.getRaw()).get(JsonObject.class, "data")
|
GsonPtr.to(mapDef.getRaw()).getAs(JsonObject.class, "data")
|
||||||
.get().addProperty("logging.properties", props);
|
.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.
|
// to access the VM's console. Might change in the future.
|
||||||
// attach(new ServiceMonitor(channel()).channelManager(chanMgr));
|
// attach(new ServiceMonitor(channel()).channelManager(chanMgr));
|
||||||
attach(new Reconciler(channel()));
|
attach(new Reconciler(channel()));
|
||||||
attach(new PoolManager(channel()));
|
attach(new PoolMonitor(channel()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -18,27 +18,28 @@
|
||||||
|
|
||||||
package org.jdrupes.vmoperator.manager;
|
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.ApiException;
|
||||||
import io.kubernetes.client.openapi.models.V1ObjectMeta;
|
|
||||||
import io.kubernetes.client.util.Watch;
|
import io.kubernetes.client.util.Watch;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
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_GROUP;
|
||||||
|
import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
|
||||||
import org.jdrupes.vmoperator.common.K8s;
|
import org.jdrupes.vmoperator.common.K8s;
|
||||||
import org.jdrupes.vmoperator.common.K8sClient;
|
import org.jdrupes.vmoperator.common.K8sClient;
|
||||||
import org.jdrupes.vmoperator.common.K8sDynamicModel;
|
import org.jdrupes.vmoperator.common.K8sDynamicModel;
|
||||||
import org.jdrupes.vmoperator.common.K8sDynamicModels;
|
import org.jdrupes.vmoperator.common.K8sDynamicModels;
|
||||||
import org.jdrupes.vmoperator.common.K8sDynamicStub;
|
import org.jdrupes.vmoperator.common.K8sDynamicStub;
|
||||||
import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
|
import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
|
||||||
|
import org.jdrupes.vmoperator.common.VmDefinitionStub;
|
||||||
import org.jdrupes.vmoperator.common.VmPool;
|
import org.jdrupes.vmoperator.common.VmPool;
|
||||||
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_KIND_VM_POOL;
|
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.VmDefChanged;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmPoolChanged;
|
import org.jdrupes.vmoperator.manager.events.VmPoolChanged;
|
||||||
import org.jdrupes.vmoperator.util.GsonPtr;
|
import org.jdrupes.vmoperator.util.GsonPtr;
|
||||||
|
|
@ -53,11 +54,9 @@ import org.jgrapes.core.events.Attached;
|
||||||
* avoid concurrent change informations.
|
* avoid concurrent change informations.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports" })
|
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports" })
|
||||||
public class PoolManager extends
|
public class PoolMonitor extends
|
||||||
AbstractMonitor<K8sDynamicModel, K8sDynamicModels, Channel> {
|
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 final Map<String, VmPool> pools = new ConcurrentHashMap<>();
|
||||||
private EventPipeline poolPipeline;
|
private EventPipeline poolPipeline;
|
||||||
|
|
||||||
|
|
@ -67,7 +66,7 @@ public class PoolManager extends
|
||||||
* @param componentChannel the component channel
|
* @param componentChannel the component channel
|
||||||
* @param channelManager the channel manager
|
* @param channelManager the channel manager
|
||||||
*/
|
*/
|
||||||
public PoolManager(Channel componentChannel) {
|
public PoolMonitor(Channel componentChannel) {
|
||||||
super(componentChannel, K8sDynamicModel.class,
|
super(componentChannel, K8sDynamicModel.class,
|
||||||
K8sDynamicModels.class);
|
K8sDynamicModels.class);
|
||||||
}
|
}
|
||||||
|
|
@ -107,18 +106,13 @@ public class PoolManager extends
|
||||||
|
|
||||||
// When pool is deleted, save VMs in pending
|
// When pool is deleted, save VMs in pending
|
||||||
if (type == ResponseType.DELETED) {
|
if (type == ResponseType.DELETED) {
|
||||||
try {
|
Optional.ofNullable(pools.get(poolName)).ifPresent(pool -> {
|
||||||
pendingLock.lock();
|
pool.setDefined(false);
|
||||||
Optional.ofNullable(pools.get(poolName)).ifPresent(
|
if (pool.vms().isEmpty()) {
|
||||||
p -> {
|
|
||||||
pending.computeIfAbsent(poolName, k -> Collections
|
|
||||||
.synchronizedSet(new HashSet<>())).addAll(p.vms());
|
|
||||||
pools.remove(poolName);
|
pools.remove(poolName);
|
||||||
poolPipeline.fire(new VmPoolChanged(p, true));
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
pendingLock.unlock();
|
|
||||||
}
|
}
|
||||||
|
poolPipeline.fire(new VmPoolChanged(pool, true));
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,75 +129,85 @@ public class PoolManager extends
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to VM pool
|
// Get pool and merge changes
|
||||||
var vmPool = client().getJSON().getGson().fromJson(
|
var vmPool = pools.computeIfAbsent(poolName, k -> new VmPool(poolName));
|
||||||
GsonPtr.to(poolModel.data()).to("spec").get(),
|
var newData = client().getJSON().getGson().fromJson(
|
||||||
VmPool.class);
|
GsonPtr.to(poolModel.data()).to("spec").get(), VmPool.class);
|
||||||
V1ObjectMeta metadata = response.object.getMetadata();
|
vmPool.setRetention(newData.retention());
|
||||||
vmPool.setName(metadata.getName());
|
vmPool.setPermissions(newData.permissions());
|
||||||
|
vmPool.setDefined(true);
|
||||||
// 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);
|
|
||||||
poolPipeline.fire(new VmPoolChanged(vmPool));
|
poolPipeline.fire(new VmPoolChanged(vmPool));
|
||||||
} finally {
|
|
||||||
pendingLock.unlock();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Track VM definition changes.
|
* Track VM definition changes.
|
||||||
*
|
*
|
||||||
* @param event the event
|
* @param event the event
|
||||||
|
* @throws ApiException
|
||||||
*/
|
*/
|
||||||
@Handler
|
@Handler
|
||||||
public void onVmDefChanged(VmDefChanged event) {
|
public void onVmDefChanged(VmDefChanged event) throws ApiException {
|
||||||
String vmName = event.vmDefinition().name();
|
final var vmDef = event.vmDefinition();
|
||||||
|
final String vmName = vmDef.name();
|
||||||
switch (event.type()) {
|
switch (event.type()) {
|
||||||
case ADDED:
|
case ADDED:
|
||||||
try {
|
vmDef.<List<String>> fromSpec("pools")
|
||||||
pendingLock.lock();
|
|
||||||
event.vmDefinition().<List<String>> fromSpec("pools")
|
|
||||||
.orElse(Collections.emptyList()).stream().forEach(p -> {
|
.orElse(Collections.emptyList()).stream().forEach(p -> {
|
||||||
if (pools.containsKey(p)) {
|
pools.computeIfAbsent(p, k -> new VmPool(p))
|
||||||
pools.get(p).vms().add(vmName);
|
.vms().add(vmName);
|
||||||
} else {
|
|
||||||
pending.computeIfAbsent(p, k -> Collections
|
|
||||||
.synchronizedSet(new HashSet<>())).add(vmName);
|
|
||||||
}
|
|
||||||
poolPipeline.fire(new VmPoolChanged(pools.get(p)));
|
poolPipeline.fire(new VmPoolChanged(pools.get(p)));
|
||||||
});
|
});
|
||||||
} finally {
|
|
||||||
pendingLock.unlock();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case DELETED:
|
case DELETED:
|
||||||
try {
|
|
||||||
pendingLock.lock();
|
|
||||||
pools.values().stream().forEach(p -> {
|
pools.values().stream().forEach(p -> {
|
||||||
if (p.vms().remove(vmName)) {
|
if (p.vms().remove(vmName)) {
|
||||||
poolPipeline.fire(new VmPoolChanged(p));
|
poolPipeline.fire(new VmPoolChanged(p));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Should not be necessary, but just in case
|
return;
|
||||||
pending.values().stream().forEach(s -> s.remove(vmName));
|
|
||||||
} finally {
|
|
||||||
pendingLock.unlock();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
break;
|
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.VmChannel;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||||
import org.jdrupes.vmoperator.util.DataPath;
|
import org.jdrupes.vmoperator.util.DataPath;
|
||||||
|
import org.jdrupes.vmoperator.util.GsonPtr;
|
||||||
import org.yaml.snakeyaml.LoaderOptions;
|
import org.yaml.snakeyaml.LoaderOptions;
|
||||||
import org.yaml.snakeyaml.Yaml;
|
import org.yaml.snakeyaml.Yaml;
|
||||||
import org.yaml.snakeyaml.constructor.SafeConstructor;
|
import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
|
|
@ -179,9 +180,13 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
var pvcDef = Dynamics.newFromYaml(
|
var pvcDef = Dynamics.newFromYaml(
|
||||||
new Yaml(new SafeConstructor(new LoaderOptions())), out.toString());
|
new Yaml(new SafeConstructor(new LoaderOptions())), out.toString());
|
||||||
|
|
||||||
// Do apply changes
|
// Apply changes
|
||||||
var pvcStub
|
var pvcStub
|
||||||
= K8sV1PvcStub.get(channel.client(), vmDef.namespace(), pvcName);
|
= 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();
|
PatchOptions opts = new PatchOptions();
|
||||||
opts.setForce(true);
|
opts.setForce(true);
|
||||||
opts.setFieldManager("kubernetes-java-kubectl-apply");
|
opts.setFieldManager("kubernetes-java-kubectl-apply");
|
||||||
|
|
@ -191,5 +196,20 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
logger.warning(
|
logger.warning(
|
||||||
() -> "Could not patch pvc for " + pvcStub.name());
|
() -> "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
|
* 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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
|
@ -18,20 +18,22 @@
|
||||||
|
|
||||||
package org.jdrupes.vmoperator.manager;
|
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.ApiException;
|
||||||
import io.kubernetes.client.openapi.models.V1ObjectMeta;
|
import io.kubernetes.client.openapi.models.V1ObjectMeta;
|
||||||
import io.kubernetes.client.util.Watch;
|
import io.kubernetes.client.util.Watch;
|
||||||
import io.kubernetes.client.util.generic.options.ListOptions;
|
import io.kubernetes.client.util.generic.options.ListOptions;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
|
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.K8s;
|
||||||
import org.jdrupes.vmoperator.common.K8sClient;
|
import org.jdrupes.vmoperator.common.K8sClient;
|
||||||
import org.jdrupes.vmoperator.common.K8sDynamicStub;
|
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.VmDefinitionModel;
|
||||||
import org.jdrupes.vmoperator.common.VmDefinitionModels;
|
import org.jdrupes.vmoperator.common.VmDefinitionModels;
|
||||||
import org.jdrupes.vmoperator.common.VmDefinitionStub;
|
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.APP_NAME;
|
||||||
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_KIND_VM;
|
|
||||||
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
|
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
|
||||||
|
import org.jdrupes.vmoperator.manager.events.AssignVm;
|
||||||
import org.jdrupes.vmoperator.manager.events.ChannelManager;
|
import org.jdrupes.vmoperator.manager.events.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.VmChannel;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
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.Channel;
|
||||||
import org.jgrapes.core.Event;
|
import org.jgrapes.core.Event;
|
||||||
|
import org.jgrapes.core.annotation.Handler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Watches for changes of VM definitions.
|
* Watches for changes of VM definitions.
|
||||||
|
|
@ -119,11 +127,6 @@ public class VmMonitor extends
|
||||||
V1ObjectMeta metadata = response.object.getMetadata();
|
V1ObjectMeta metadata = response.object.getMetadata();
|
||||||
VmChannel channel = channelManager.channelGet(metadata.getName());
|
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
|
// Get full definition and associate with channel as backup
|
||||||
var vmModel = response.object;
|
var vmModel = response.object;
|
||||||
if (vmModel.data() == null) {
|
if (vmModel.data() == null) {
|
||||||
|
|
@ -151,17 +154,16 @@ public class VmMonitor extends
|
||||||
|
|
||||||
// Create and fire changed event. Remove channel from channel
|
// Create and fire changed event. Remove channel from channel
|
||||||
// manager on completion.
|
// manager on completion.
|
||||||
channel.pipeline()
|
VmDefChanged chgEvt
|
||||||
.fire(Event.onCompletion(
|
= new VmDefChanged(ResponseType.valueOf(response.type),
|
||||||
new VmDefChanged(ResponseType.valueOf(response.type),
|
|
||||||
channel.setGeneration(response.object.getMetadata()
|
channel.setGeneration(response.object.getMetadata()
|
||||||
.getGeneration()),
|
.getGeneration()),
|
||||||
vmDef),
|
vmDef);
|
||||||
e -> {
|
if (ResponseType.valueOf(response.type) == ResponseType.DELETED) {
|
||||||
if (e.type() == ResponseType.DELETED) {
|
chgEvt = Event.onCompletion(chgEvt,
|
||||||
channelManager.remove(e.vmDefinition().name());
|
e -> channelManager.remove(e.vmDefinition().name()));
|
||||||
}
|
}
|
||||||
}), channel);
|
channel.pipeline().fire(chgEvt, channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
private VmDefinitionModel getModel(K8sClient client,
|
private VmDefinitionModel getModel(K8sClient client,
|
||||||
|
|
@ -190,16 +192,7 @@ public class VmMonitor extends
|
||||||
// VM definition status changes before the pod terminates.
|
// VM definition status changes before the pod terminates.
|
||||||
// This results in pod information being shown for a stopped
|
// This results in pod information being shown for a stopped
|
||||||
// VM which is irritating. So check condition first.
|
// VM which is irritating. So check condition first.
|
||||||
@SuppressWarnings("PMD.LambdaCanBeMethodReference")
|
if (!vmDef.conditionStatus("Running").orElse(false)) {
|
||||||
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) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var podSearch = new ListOptions();
|
var podSearch = new ListOptions();
|
||||||
|
|
@ -227,4 +220,131 @@ public class VmMonitor extends
|
||||||
() -> "Cannot access node information: " + e.getMessage());
|
() -> "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.JsonObject;
|
||||||
import com.google.gson.JsonPrimitive;
|
import com.google.gson.JsonPrimitive;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
@ -62,7 +63,8 @@ public class GsonPtr {
|
||||||
* @param selectors the selectors
|
* @param selectors the selectors
|
||||||
* @return the Gson pointer
|
* @return the Gson pointer
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({ "PMD.ShortMethodName", "PMD.PreserveStackTrace" })
|
@SuppressWarnings({ "PMD.ShortMethodName", "PMD.PreserveStackTrace",
|
||||||
|
"PMD.AvoidDuplicateLiterals" })
|
||||||
public GsonPtr to(Object... selectors) {
|
public GsonPtr to(Object... selectors) {
|
||||||
JsonElement element = position;
|
JsonElement element = position;
|
||||||
for (Object sel : selectors) {
|
for (Object sel : selectors) {
|
||||||
|
|
@ -91,6 +93,42 @@ public class GsonPtr {
|
||||||
return new GsonPtr(element);
|
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.
|
* Returns {@link JsonElement} that the pointer points to.
|
||||||
*
|
*
|
||||||
|
|
@ -109,7 +147,7 @@ public class GsonPtr {
|
||||||
* @return the result
|
* @return the result
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" })
|
@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())) {
|
if (cls.isAssignableFrom(position.getClass())) {
|
||||||
return cls.cast(position);
|
return cls.cast(position);
|
||||||
}
|
}
|
||||||
|
|
@ -128,7 +166,7 @@ public class GsonPtr {
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" })
|
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" })
|
||||||
public <T extends JsonElement> Optional<T>
|
public <T extends JsonElement> Optional<T>
|
||||||
get(Class<T> cls, Object... selectors) {
|
getAs(Class<T> cls, Object... selectors) {
|
||||||
JsonElement element = position;
|
JsonElement element = position;
|
||||||
for (Object sel : selectors) {
|
for (Object sel : selectors) {
|
||||||
if (element instanceof JsonObject obj
|
if (element instanceof JsonObject obj
|
||||||
|
|
@ -163,7 +201,7 @@ public class GsonPtr {
|
||||||
* @return the as string
|
* @return the as string
|
||||||
*/
|
*/
|
||||||
public Optional<String> getAsString(Object... selectors) {
|
public Optional<String> getAsString(Object... selectors) {
|
||||||
return get(JsonPrimitive.class, selectors)
|
return getAs(JsonPrimitive.class, selectors)
|
||||||
.map(JsonPrimitive::getAsString);
|
.map(JsonPrimitive::getAsString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,7 +212,7 @@ public class GsonPtr {
|
||||||
* @return the as string
|
* @return the as string
|
||||||
*/
|
*/
|
||||||
public Optional<Integer> getAsInt(Object... selectors) {
|
public Optional<Integer> getAsInt(Object... selectors) {
|
||||||
return get(JsonPrimitive.class, selectors)
|
return getAs(JsonPrimitive.class, selectors)
|
||||||
.map(JsonPrimitive::getAsInt);
|
.map(JsonPrimitive::getAsInt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -185,7 +223,7 @@ public class GsonPtr {
|
||||||
* @return the as string
|
* @return the as string
|
||||||
*/
|
*/
|
||||||
public Optional<BigInteger> getAsBigInteger(Object... selectors) {
|
public Optional<BigInteger> getAsBigInteger(Object... selectors) {
|
||||||
return get(JsonPrimitive.class, selectors)
|
return getAs(JsonPrimitive.class, selectors)
|
||||||
.map(JsonPrimitive::getAsBigInteger);
|
.map(JsonPrimitive::getAsBigInteger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,7 +234,7 @@ public class GsonPtr {
|
||||||
* @return the as string
|
* @return the as string
|
||||||
*/
|
*/
|
||||||
public Optional<Long> getAsLong(Object... selectors) {
|
public Optional<Long> getAsLong(Object... selectors) {
|
||||||
return get(JsonPrimitive.class, selectors)
|
return getAs(JsonPrimitive.class, selectors)
|
||||||
.map(JsonPrimitive::getAsLong);
|
.map(JsonPrimitive::getAsLong);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -207,7 +245,7 @@ public class GsonPtr {
|
||||||
* @return the boolean
|
* @return the boolean
|
||||||
*/
|
*/
|
||||||
public Optional<Boolean> getAsBoolean(Object... selectors) {
|
public Optional<Boolean> getAsBoolean(Object... selectors) {
|
||||||
return get(JsonPrimitive.class, selectors)
|
return getAs(JsonPrimitive.class, selectors)
|
||||||
.map(JsonPrimitive::getAsBoolean);
|
.map(JsonPrimitive::getAsBoolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,7 +260,7 @@ public class GsonPtr {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public <T extends JsonElement> List<T> getAsListOf(Class<T> cls,
|
public <T extends JsonElement> List<T> getAsListOf(Class<T> cls,
|
||||||
Object... selectors) {
|
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());
|
.orElse(Collections.emptyList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -336,4 +374,22 @@ public class GsonPtr {
|
||||||
return this;
|
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">
|
data-jgwc-on-unload="JGConsole.jgwc.unmountVueApps">
|
||||||
<form :id="formId" ref="formDom" onsubmit="return false;">
|
<form :id="formId" ref="formDom" onsubmit="return false;">
|
||||||
<section>
|
<section>
|
||||||
<span>{{ localize("Select VM") }}</span>
|
<fieldset>
|
||||||
<p>
|
<legend>{{ localize("Select VM or pool") }}</legend>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
<label>
|
<label>
|
||||||
|
<input v-model="resource" type="radio" value="vm"
|
||||||
|
<#if vmNames?size == 0>:disabled="true"</#if>label>
|
||||||
<span>{{ localize("VM") }}</span>
|
<span>{{ localize("VM") }}</span>
|
||||||
<select v-model="vmNameInput">
|
<select v-model="vmNameInput" :disabled="resource !== 'vm'">
|
||||||
<#list vmNames as name>
|
<#list vmNames as name>
|
||||||
<option value="${name}">${name}</option>
|
<option value="${name}">${name}</option>
|
||||||
</#list>
|
</#list>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</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>
|
</section>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,3 +5,5 @@ okayLabel = Apply and Close
|
||||||
confirmResetTitle = Confirm reset
|
confirmResetTitle = Confirm reset
|
||||||
confirmResetMsg = Resetting the VM may cause loss of data. \
|
confirmResetMsg = Resetting the VM may cause loss of data. \
|
||||||
Please confirm to continue.
|
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
|
conletName = VM-Zugriff
|
||||||
|
|
||||||
okayLabel = Anwenden und Schließen
|
okayLabel = Anwenden und Schließen
|
||||||
Select\ VM = VM auswählen
|
Select\ VM\ or\ pool = VM oder Pool auswählen
|
||||||
|
|
||||||
Start\ VM = VM starten
|
Start\ VM = VM starten
|
||||||
Stop\ VM = VM anhalten
|
Stop\ VM = VM anhalten
|
||||||
|
|
@ -11,3 +11,7 @@ Open\ console = Konsole anzeigen
|
||||||
confirmResetTitle = Zurücksetzen bestätigen
|
confirmResetTitle = Zurücksetzen bestätigen
|
||||||
confirmResetMsg = Zurücksetzen der VM kann zu Datenverlust führen. \
|
confirmResetMsg = Zurücksetzen der VM kann zu Datenverlust führen. \
|
||||||
Bitte bestätigen um fortzufahren.
|
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
|
* 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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as
|
* 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.K8sObserver;
|
||||||
import org.jdrupes.vmoperator.common.VmDefinition;
|
import org.jdrupes.vmoperator.common.VmDefinition;
|
||||||
import org.jdrupes.vmoperator.common.VmDefinition.Permission;
|
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.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.ModifyVm;
|
||||||
import org.jdrupes.vmoperator.manager.events.ResetVm;
|
import org.jdrupes.vmoperator.manager.events.ResetVm;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||||
|
import org.jdrupes.vmoperator.manager.events.VmPoolChanged;
|
||||||
import org.jgrapes.core.Channel;
|
import org.jgrapes.core.Channel;
|
||||||
import org.jgrapes.core.Components;
|
import org.jgrapes.core.Components;
|
||||||
import org.jgrapes.core.Event;
|
import org.jgrapes.core.Event;
|
||||||
|
import org.jgrapes.core.EventPipeline;
|
||||||
import org.jgrapes.core.Manager;
|
import org.jgrapes.core.Manager;
|
||||||
import org.jgrapes.core.annotation.Handler;
|
import org.jgrapes.core.annotation.Handler;
|
||||||
|
import org.jgrapes.core.events.Start;
|
||||||
import org.jgrapes.http.Session;
|
import org.jgrapes.http.Session;
|
||||||
import org.jgrapes.util.events.ConfigurationUpdate;
|
import org.jgrapes.util.events.ConfigurationUpdate;
|
||||||
import org.jgrapes.util.events.KeyValueStoreQuery;
|
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.ConsolePrepared;
|
||||||
import org.jgrapes.webconsole.base.events.ConsoleReady;
|
import org.jgrapes.webconsole.base.events.ConsoleReady;
|
||||||
import org.jgrapes.webconsole.base.events.DeleteConlet;
|
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.NotifyConletModel;
|
||||||
import org.jgrapes.webconsole.base.events.NotifyConletView;
|
import org.jgrapes.webconsole.base.events.NotifyConletView;
|
||||||
import org.jgrapes.webconsole.base.events.OpenModalDialog;
|
import org.jgrapes.webconsole.base.events.OpenModalDialog;
|
||||||
|
|
@ -106,10 +114,12 @@ import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet;
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports",
|
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports",
|
||||||
"PMD.CouplingBetweenObjects", "PMD.GodClass", "PMD.TooManyMethods" })
|
"PMD.CouplingBetweenObjects", "PMD.GodClass", "PMD.TooManyMethods",
|
||||||
public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
"PMD.CyclomaticComplexity" })
|
||||||
|
public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
|
||||||
|
|
||||||
private static final String VM_NAME_PROPERTY = "vmName";
|
private static final String VM_NAME_PROPERTY = "vmName";
|
||||||
|
private static final String POOL_NAME_PROPERTY = "poolName";
|
||||||
private static final String RENDERED
|
private static final String RENDERED
|
||||||
= VmAccess.class.getName() + ".rendered";
|
= VmAccess.class.getName() + ".rendered";
|
||||||
private static final String PENDING
|
private static final String PENDING
|
||||||
|
|
@ -118,8 +128,7 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
||||||
RenderMode.Preview, RenderMode.Edit);
|
RenderMode.Preview, RenderMode.Edit);
|
||||||
private static final Set<RenderMode> MODES_FOR_GENERATED = RenderMode.asSet(
|
private static final Set<RenderMode> MODES_FOR_GENERATED = RenderMode.asSet(
|
||||||
RenderMode.Preview, RenderMode.StickyPreview);
|
RenderMode.Preview, RenderMode.StickyPreview);
|
||||||
private final ChannelTracker<String, VmChannel,
|
private EventPipeline appPipeline;
|
||||||
VmDefinition> channelTracker = new ChannelTracker<>();
|
|
||||||
private static ObjectMapper objectMapper
|
private static ObjectMapper objectMapper
|
||||||
= new ObjectMapper().registerModule(new JavaTimeModule());
|
= new ObjectMapper().registerModule(new JavaTimeModule());
|
||||||
private Class<?> preferredIpVersion = Inet4Address.class;
|
private Class<?> preferredIpVersion = Inet4Address.class;
|
||||||
|
|
@ -144,6 +153,16 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
||||||
super(componentChannel);
|
super(componentChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On start.
|
||||||
|
*
|
||||||
|
* @param event the event
|
||||||
|
*/
|
||||||
|
@Handler
|
||||||
|
public void onStart(Start event) {
|
||||||
|
appPipeline = event.processedBy().get();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configure the component.
|
* Configure the component.
|
||||||
*
|
*
|
||||||
|
|
@ -247,36 +266,74 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
||||||
* @throws InterruptedException the interrupted exception
|
* @throws InterruptedException the interrupted exception
|
||||||
*/
|
*/
|
||||||
@Handler
|
@Handler
|
||||||
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
|
|
||||||
public void onConsoleConfigured(ConsoleConfigured event,
|
public void onConsoleConfigured(ConsoleConfigured event,
|
||||||
ConsoleConnection connection) throws InterruptedException,
|
ConsoleConnection connection) throws InterruptedException,
|
||||||
IOException {
|
IOException {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
final var rendered = (Set<String>) connection.session().get(RENDERED);
|
final var rendered
|
||||||
|
= (Set<ResourceModel>) connection.session().get(RENDERED);
|
||||||
connection.session().remove(RENDERED);
|
connection.session().remove(RENDERED);
|
||||||
if (!syncPreviews(connection.session())) {
|
if (!syncPreviews(connection.session())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
addMissingConlets(event, connection, rendered);
|
||||||
boolean foundMissing = false;
|
|
||||||
for (var vmName : accessibleVms(connection)) {
|
|
||||||
if (rendered.contains(vmName)) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
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
|
// Suspending to allow rendering of conlets to be noticed
|
||||||
var failSafe = Components.schedule(t -> event.resumeHandling(),
|
var failSafe = Components.schedule(t -> event.resumeHandling(),
|
||||||
Duration.ofSeconds(1));
|
Duration.ofSeconds(1));
|
||||||
event.suspendHandling(failSafe::cancel);
|
event.suspendHandling(failSafe::cancel);
|
||||||
connection.setAssociated(PENDING, event);
|
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(),
|
fire(new AddConletRequest(event.event().event().renderSupport(),
|
||||||
VmAccess.class.getName(),
|
VmAccess.class.getName(), RenderMode.asSet(RenderMode.Preview))
|
||||||
RenderMode.asSet(RenderMode.Preview))
|
|
||||||
.addProperty(VM_NAME_PROPERTY, vmName),
|
.addProperty(VM_NAME_PROPERTY, vmName),
|
||||||
connection);
|
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
|
@Override
|
||||||
protected Optional<ViewerModel> createNewState(AddConletRequest event,
|
protected Optional<ResourceModel> createNewState(AddConletRequest event,
|
||||||
ConsoleConnection connection, String conletId) throws Exception {
|
ConsoleConnection connection, String conletId) throws Exception {
|
||||||
var model = new ViewerModel(conletId);
|
var model = new ResourceModel(conletId);
|
||||||
model.vmName = (String) event.properties().get(VM_NAME_PROPERTY);
|
var poolName = (String) event.properties().get(POOL_NAME_PROPERTY);
|
||||||
if (model.vmName != null) {
|
if (poolName != null) {
|
||||||
model.setGenerated(true);
|
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);
|
String jsonState = objectMapper.writeValueAsString(model);
|
||||||
connection.respond(new KeyValueStoreUpdate().update(
|
connection.respond(new KeyValueStoreUpdate().update(
|
||||||
|
|
@ -314,9 +375,9 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Optional<ViewerModel> createStateRepresentation(Event<?> event,
|
protected Optional<ResourceModel> createStateRepresentation(Event<?> event,
|
||||||
ConsoleConnection connection, String conletId) throws Exception {
|
ConsoleConnection connection, String conletId) throws Exception {
|
||||||
var model = new ViewerModel(conletId);
|
var model = new ResourceModel(conletId);
|
||||||
String jsonState = objectMapper.writeValueAsString(model);
|
String jsonState = objectMapper.writeValueAsString(model);
|
||||||
connection.respond(new KeyValueStoreUpdate().update(
|
connection.respond(new KeyValueStoreUpdate().update(
|
||||||
storagePath(connection.session(), model.getConletId()), jsonState));
|
storagePath(connection.session(), model.getConletId()), jsonState));
|
||||||
|
|
@ -325,7 +386,7 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings("PMD.EmptyCatchBlock")
|
@SuppressWarnings("PMD.EmptyCatchBlock")
|
||||||
protected Optional<ViewerModel> recreateState(Event<?> event,
|
protected Optional<ResourceModel> recreateState(Event<?> event,
|
||||||
ConsoleConnection channel, String conletId) throws Exception {
|
ConsoleConnection channel, String conletId) throws Exception {
|
||||||
KeyValueStoreQuery query = new KeyValueStoreQuery(
|
KeyValueStoreQuery query = new KeyValueStoreQuery(
|
||||||
storagePath(channel.session(), conletId), channel);
|
storagePath(channel.session(), conletId), channel);
|
||||||
|
|
@ -334,8 +395,8 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
||||||
if (!query.results().isEmpty()) {
|
if (!query.results().isEmpty()) {
|
||||||
var json = query.results().get(0).values().stream().findFirst()
|
var json = query.results().get(0).values().stream().findFirst()
|
||||||
.get();
|
.get();
|
||||||
ViewerModel model
|
ResourceModel model
|
||||||
= objectMapper.readValue(json, ViewerModel.class);
|
= objectMapper.readValue(json, ResourceModel.class);
|
||||||
return Optional.of(model);
|
return Optional.of(model);
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
|
@ -347,34 +408,85 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops", "unchecked" })
|
@SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops" })
|
||||||
protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event,
|
protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event,
|
||||||
ConsoleConnection channel, String conletId, ViewerModel model)
|
ConsoleConnection channel, String conletId, ResourceModel model)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
|
if (event.renderAs().contains(RenderMode.Preview)) {
|
||||||
|
return renderPreview(event, channel, conletId, model);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render edit
|
||||||
ResourceBundle resourceBundle = resourceBundle(channel.locale());
|
ResourceBundle resourceBundle = resourceBundle(channel.locale());
|
||||||
Set<RenderMode> renderedAs = EnumSet.noneOf(RenderMode.class);
|
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)
|
channel.associated(PENDING, Event.class)
|
||||||
.ifPresent(e -> {
|
.ifPresent(e -> {
|
||||||
e.resumeHandling();
|
e.resumeHandling();
|
||||||
channel.setAssociated(PENDING, null);
|
channel.setAssociated(PENDING, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove conlet if definition has been removed
|
VmDefinition vmDef = null;
|
||||||
if (model.vmName() != null
|
if (model.mode() == ResourceModel.Mode.VM && model.name() != null) {
|
||||||
&& !channelTracker.associated(model.vmName()).isPresent()) {
|
// 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(
|
channel.respond(
|
||||||
new DeleteConlet(conletId, Collections.emptySet()));
|
new DeleteConlet(conletId, Collections.emptySet()));
|
||||||
return Collections.emptySet();
|
return Collections.emptySet();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Don't render if user has not at least one permission
|
if (model.mode() == ResourceModel.Mode.POOL && model.name() != null) {
|
||||||
if (model.vmName() != null
|
// Remove conlet if pool definition has been removed
|
||||||
&& channelTracker.associated(model.vmName())
|
// or user has not at least one permission
|
||||||
.map(d -> permissions(d, channel.session()).isEmpty())
|
VmPool pool = appPipeline
|
||||||
.orElse(true)) {
|
.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();
|
return Collections.emptySet();
|
||||||
}
|
}
|
||||||
|
vmDef = getVmData(model, channel).map(VmData::definition)
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
// Render
|
// Render
|
||||||
Template tpl
|
Template tpl
|
||||||
|
|
@ -387,31 +499,32 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
||||||
.setSupportedModes(syncPreviews(channel.session())
|
.setSupportedModes(syncPreviews(channel.session())
|
||||||
? MODES_FOR_GENERATED
|
? MODES_FOR_GENERATED
|
||||||
: MODES));
|
: MODES));
|
||||||
renderedAs.add(RenderMode.Preview);
|
if (!Strings.isNullOrEmpty(model.name())) {
|
||||||
if (!Strings.isNullOrEmpty(model.vmName())) {
|
|
||||||
Optional.ofNullable(channel.session().get(RENDERED))
|
Optional.ofNullable(channel.session().get(RENDERED))
|
||||||
.ifPresent(s -> ((Set<String>) s).add(model.vmName()));
|
.ifPresent(s -> ((Set<ResourceModel>) s).add(model));
|
||||||
updateConfig(channel, model);
|
updatePreview(channel, model, vmDef);
|
||||||
}
|
}
|
||||||
}
|
return EnumSet.of(RenderMode.Preview);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> accessibleVms(ConsoleConnection channel) {
|
private Optional<VmData> getVmData(ResourceModel model,
|
||||||
return channelTracker.associated().stream()
|
ConsoleConnection channel) throws InterruptedException {
|
||||||
.filter(d -> !permissions(d, channel.session()).isEmpty())
|
if (model.mode() == ResourceModel.Mode.VM) {
|
||||||
.map(d -> d.getMetadata().getName()).sorted().toList();
|
// 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) {
|
private Set<Permission> permissions(VmDefinition vmDef, Session session) {
|
||||||
|
|
@ -422,39 +535,83 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
||||||
return vmDef.permissionsFor(user, roles);
|
return vmDef.permissionsFor(user, roles);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateConfig(ConsoleConnection channel, ViewerModel model) {
|
private Set<Permission> permissions(VmPool pool, Session session) {
|
||||||
channel.respond(new NotifyConletView(type(),
|
var user = WebConsoleUtils.userFromSession(session)
|
||||||
model.getConletId(), "updateConfig", model.vmName()));
|
.map(ConsoleUser::getName).orElse(null);
|
||||||
updateVmDef(channel, model);
|
var roles = WebConsoleUtils.rolesFromSession(session)
|
||||||
|
.stream().map(ConsoleRole::getName).toList();
|
||||||
|
return pool.permissionsFor(user, roles);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateVmDef(ConsoleConnection channel, ViewerModel model) {
|
private Set<Permission> permissions(ResourceModel model, Session session,
|
||||||
if (Strings.isNullOrEmpty(model.vmName())) {
|
VmPool pool, VmDefinition vmDef) throws InterruptedException {
|
||||||
return;
|
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 {
|
try {
|
||||||
var vmDef = item.associated();
|
data = Map.of("metadata",
|
||||||
var data = Map.of("metadata",
|
|
||||||
Map.of("namespace", vmDef.namespace(),
|
Map.of("namespace", vmDef.namespace(),
|
||||||
"name", vmDef.name()),
|
"name", vmDef.name()),
|
||||||
"spec", vmDef.spec(),
|
"spec", vmDef.spec(),
|
||||||
"status", vmDef.getStatus(),
|
"status", vmDef.getStatus());
|
||||||
"userPermissions",
|
|
||||||
permissions(vmDef, channel.session()).stream()
|
|
||||||
.map(Permission::toString).toList());
|
|
||||||
channel.respond(new NotifyConletView(type(),
|
|
||||||
model.getConletId(), "updateVmDefinition", data));
|
|
||||||
} catch (JsonSyntaxException e) {
|
} catch (JsonSyntaxException e) {
|
||||||
logger.log(Level.SEVERE, e,
|
logger.log(Level.SEVERE, e,
|
||||||
() -> "Failed to serialize VM definition");
|
() -> "Failed to serialize VM definition");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
channel.respond(new NotifyConletView(type(),
|
||||||
|
model.getConletId(), "updateVmDefinition", data));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doConletDeleted(ConletDeleted event,
|
protected void doConletDeleted(ConletDeleted event,
|
||||||
ConsoleConnection channel, String conletId, ViewerModel conletState)
|
ConsoleConnection channel, String conletId,
|
||||||
|
ResourceModel conletState)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
if (event.renderModes().isEmpty()) {
|
if (event.renderModes().isEmpty()) {
|
||||||
channel.respond(new KeyValueStoreUpdate().delete(
|
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 event the event
|
||||||
* @param channel the channel
|
* @param channel the channel
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
|
* @throws InterruptedException
|
||||||
*/
|
*/
|
||||||
@Handler(namedChannels = "manager")
|
@Handler(namedChannels = "manager")
|
||||||
@SuppressWarnings({ "PMD.ConfusingTernary", "PMD.CognitiveComplexity",
|
@SuppressWarnings({ "PMD.ConfusingTernary", "PMD.CognitiveComplexity",
|
||||||
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals",
|
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals",
|
||||||
"PMD.ConfusingArgumentToVarargsMethod" })
|
"PMD.ConfusingArgumentToVarargsMethod" })
|
||||||
public void onVmDefChanged(VmDefChanged event, VmChannel channel)
|
public void onVmDefChanged(VmDefChanged event, VmChannel channel)
|
||||||
throws IOException {
|
throws IOException, InterruptedException {
|
||||||
var vmDef = event.vmDefinition();
|
var vmDef = event.vmDefinition();
|
||||||
var vmName = vmDef.name();
|
|
||||||
if (event.type() == K8sObserver.ResponseType.DELETED) {
|
// Update known conlets
|
||||||
channelTracker.remove(vmName);
|
|
||||||
} else {
|
|
||||||
channelTracker.put(vmName, channel, vmDef);
|
|
||||||
}
|
|
||||||
for (var entry : conletIdsByConsoleConnection().entrySet()) {
|
for (var entry : conletIdsByConsoleConnection().entrySet()) {
|
||||||
var connection = entry.getKey();
|
var connection = entry.getKey();
|
||||||
for (var conletId : entry.getValue()) {
|
for (var conletId : entry.getValue()) {
|
||||||
var model = stateFromSession(connection.session(), conletId);
|
var model = stateFromSession(connection.session(), conletId);
|
||||||
if (model.isEmpty()
|
if (model.isEmpty()
|
||||||
|| !Objects.areEqual(model.get().vmName(), vmName)) {
|
|| Strings.isNullOrEmpty(model.get().name())) {
|
||||||
continue;
|
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(
|
connection.respond(
|
||||||
new DeleteConlet(conletId, Collections.emptySet()));
|
new DeleteConlet(conletId, Collections.emptySet()));
|
||||||
} else {
|
continue;
|
||||||
updateVmDef(connection, model.get());
|
|
||||||
}
|
}
|
||||||
|
} 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",
|
* On vm pool changed.
|
||||||
"PMD.ConfusingArgumentToVarargsMethod", "PMD.NcssCount",
|
*
|
||||||
|
* @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" })
|
"PMD.AvoidLiteralsInIfCondition" })
|
||||||
|
@Override
|
||||||
protected void doUpdateConletState(NotifyConletModel event,
|
protected void doUpdateConletState(NotifyConletModel event,
|
||||||
ConsoleConnection channel, ViewerModel model)
|
ConsoleConnection channel, ResourceModel model) throws Exception {
|
||||||
throws Exception {
|
|
||||||
event.stop();
|
event.stop();
|
||||||
if ("selectedVm".equals(event.method())) {
|
if ("selectedResource".equals(event.method())) {
|
||||||
selectVm(event, channel, model);
|
selectResource(event, channel, model);
|
||||||
return;
|
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
|
// Handle command for selected VM
|
||||||
var both = Optional.ofNullable(model.vmName())
|
var vmChannel = vmData.get().channel();
|
||||||
.flatMap(vm -> channelTracker.value(vm));
|
var vmDef = vmData.get().definition();
|
||||||
if (both.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var vmChannel = both.get().channel();
|
|
||||||
var vmDef = both.get().associated();
|
|
||||||
var vmName = vmDef.metadata().getName();
|
var vmName = vmDef.metadata().getName();
|
||||||
var perms = permissions(vmDef, channel.session());
|
var perms = permissions(model, channel.session(), null, vmDef);
|
||||||
var resourceBundle = resourceBundle(channel.locale());
|
var resourceBundle = resourceBundle(channel.locale());
|
||||||
switch (event.method()) {
|
switch (event.method()) {
|
||||||
case "start":
|
case "start":
|
||||||
if (perms.contains(Permission.START)) {
|
if (perms.contains(VmDefinition.Permission.START)) {
|
||||||
fire(new ModifyVm(vmName, "state", "Running", vmChannel));
|
fire(new ModifyVm(vmName, "state", "Running", vmChannel));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "stop":
|
case "stop":
|
||||||
if (perms.contains(Permission.STOP)) {
|
if (perms.contains(VmDefinition.Permission.STOP)) {
|
||||||
fire(new ModifyVm(vmName, "state", "Stopped", vmChannel));
|
fire(new ModifyVm(vmName, "state", "Stopped", vmChannel));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "reset":
|
case "reset":
|
||||||
if (perms.contains(Permission.RESET)) {
|
if (perms.contains(VmDefinition.Permission.RESET)) {
|
||||||
confirmReset(event, channel, model, resourceBundle);
|
confirmReset(event, channel, model, resourceBundle);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "resetConfirmed":
|
case "resetConfirmed":
|
||||||
if (perms.contains(Permission.RESET)) {
|
if (perms.contains(VmDefinition.Permission.RESET)) {
|
||||||
fire(new ResetVm(vmName), vmChannel);
|
fire(new ResetVm(vmName), vmChannel);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "openConsole":
|
case "openConsole":
|
||||||
if (perms.contains(Permission.ACCESS_CONSOLE)) {
|
|
||||||
var user = WebConsoleUtils.userFromSession(channel.session())
|
var user = WebConsoleUtils.userFromSession(channel.session())
|
||||||
.map(ConsoleUser::getName).orElse("");
|
.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
|
var pwQuery
|
||||||
= Event.onCompletion(new GetDisplayPassword(vmDef, user),
|
= Event.onCompletion(new GetDisplayPassword(vmDef, user),
|
||||||
e -> openConsole(vmName, channel, model,
|
e -> openConsole(vmDef, channel, model,
|
||||||
e.password().orElse(null)));
|
e.password().orElse(null)));
|
||||||
fire(pwQuery, vmChannel);
|
fire(pwQuery, vmChannel);
|
||||||
}
|
}
|
||||||
|
|
@ -561,30 +804,41 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void selectVm(NotifyConletModel event, ConsoleConnection channel,
|
@SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition",
|
||||||
ViewerModel model) throws JsonProcessingException {
|
"PMD.UseLocaleWithCaseConversions" })
|
||||||
model.setVmName(event.param(0));
|
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);
|
String jsonState = objectMapper.writeValueAsString(model);
|
||||||
channel.respond(new KeyValueStoreUpdate().update(storagePath(
|
channel.respond(new KeyValueStoreUpdate().update(storagePath(
|
||||||
channel.session(), model.getConletId()), jsonState));
|
channel.session(), model.getConletId()), jsonState));
|
||||||
updateConfig(channel, model);
|
updatePreview(channel, model,
|
||||||
|
getVmData(model, channel).map(VmData::definition).orElse(null));
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
logger.warning(() -> "Invalid resource type: " + e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openConsole(String vmName, ConsoleConnection connection,
|
private void openConsole(VmDefinition vmDef, ConsoleConnection connection,
|
||||||
ViewerModel model, String password) {
|
ResourceModel model, String password) {
|
||||||
var vmDef = channelTracker.associated(vmName).orElse(null);
|
|
||||||
if (vmDef == null) {
|
if (vmDef == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var addr = displayIp(vmDef);
|
var addr = displayIp(vmDef);
|
||||||
if (addr.isEmpty()) {
|
if (addr.isEmpty()) {
|
||||||
logger.severe(() -> "Failed to find display IP for " + vmName);
|
logger
|
||||||
|
.severe(() -> "Failed to find display IP for " + vmDef.name());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var port = vmDef.<Number> fromVm("display", "spice", "port")
|
var port = vmDef.<Number> fromVm("display", "spice", "port")
|
||||||
.map(Number::longValue);
|
.map(Number::longValue);
|
||||||
if (port.isEmpty()) {
|
if (port.isEmpty()) {
|
||||||
logger.severe(() -> "No port defined for display of " + vmName);
|
logger
|
||||||
|
.severe(() -> "No port defined for display of " + vmDef.name());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
StringBuffer data = new StringBuffer(100)
|
StringBuffer data = new StringBuffer(100)
|
||||||
|
|
@ -642,7 +896,7 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void confirmReset(NotifyConletModel event,
|
private void confirmReset(NotifyConletModel event,
|
||||||
ConsoleConnection channel, ViewerModel model,
|
ConsoleConnection channel, ResourceModel model,
|
||||||
ResourceBundle resourceBundle) throws TemplateNotFoundException,
|
ResourceBundle resourceBundle) throws TemplateNotFoundException,
|
||||||
MalformedTemplateNameException, ParseException, IOException {
|
MalformedTemplateNameException, ParseException, IOException {
|
||||||
Template tpl = freemarkerConfig()
|
Template tpl = freemarkerConfig()
|
||||||
|
|
@ -662,59 +916,119 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Class VmsModel.
|
* The Class AccessModel.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("PMD.DataClass")
|
@SuppressWarnings("PMD.DataClass")
|
||||||
public static class ViewerModel extends ConletBaseModel {
|
public static class ResourceModel extends ConletBaseModel {
|
||||||
|
|
||||||
private String vmName;
|
|
||||||
private boolean generated;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
* @param conletId the conlet id
|
||||||
*/
|
*/
|
||||||
public ViewerModel(@JsonProperty("conletId") String conletId) {
|
public ResourceModel(@JsonProperty("conletId") String conletId) {
|
||||||
super(conletId);
|
super(conletId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the vm name.
|
* Returns the mode.
|
||||||
*
|
*
|
||||||
* @return the vmName
|
* @return the resourceType
|
||||||
*/
|
*/
|
||||||
@JsonGetter("vmName")
|
@JsonGetter("mode")
|
||||||
public String vmName() {
|
public Mode mode() {
|
||||||
return vmName;
|
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) {
|
public void setMode(Mode mode) {
|
||||||
this.vmName = vmName;
|
this.mode = mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if is generated.
|
* Gets the resource name.
|
||||||
*
|
*
|
||||||
* @return the generated
|
* @return the string
|
||||||
*/
|
*/
|
||||||
public boolean isGenerated() {
|
@JsonGetter("name")
|
||||||
return generated;
|
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) {
|
public void setName(String name) {
|
||||||
this.generated = generated;
|
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 */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
vmName: string;
|
vmName: string;
|
||||||
vmDefinition: any;
|
vmDefinition: any;
|
||||||
|
poolName: string | null;
|
||||||
|
permissions: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const localize = (key: string) => {
|
const localize = (key: string) => {
|
||||||
|
|
@ -62,24 +64,33 @@ window.orgJDrupesVmOperatorVmAccess.initPreview = (previewDom: HTMLElement,
|
||||||
|
|
||||||
const previewApi: Api = reactive({
|
const previewApi: Api = reactive({
|
||||||
vmName: "",
|
vmName: "",
|
||||||
vmDefinition: {}
|
vmDefinition: {},
|
||||||
|
poolName: null,
|
||||||
|
permissions: []
|
||||||
});
|
});
|
||||||
|
const poolName = computed(() => previewApi.poolName);
|
||||||
|
const vmName = computed(() => previewApi.vmDefinition.name);
|
||||||
const configured = computed(() => previewApi.vmDefinition.spec);
|
const configured = computed(() => previewApi.vmDefinition.spec);
|
||||||
const startable = computed(() => previewApi.vmDefinition.spec &&
|
const busy = computed(() => previewApi.vmDefinition.spec
|
||||||
previewApi.vmDefinition.spec.vm.state !== 'Running'
|
&& (previewApi.vmDefinition.spec.vm.state === 'Running'
|
||||||
&& !previewApi.vmDefinition.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 &&
|
const stoppable = computed(() => previewApi.vmDefinition.spec &&
|
||||||
previewApi.vmDefinition.spec.vm.state !== 'Stopped'
|
previewApi.vmDefinition.spec.vm.state !== 'Stopped'
|
||||||
&& previewApi.vmDefinition.running);
|
&& previewApi.vmDefinition.running);
|
||||||
const running = computed(() => previewApi.vmDefinition.running);
|
const running = computed(() => previewApi.vmDefinition.running);
|
||||||
const inUse = computed(() => previewApi.vmDefinition.usedBy != '');
|
const inUse = computed(() => previewApi.vmDefinition.usedBy != '');
|
||||||
const permissions = computed(() => previewApi.vmDefinition.spec
|
const permissions = computed(() => previewApi.permissions);
|
||||||
? previewApi.vmDefinition.userPermissions : []);
|
|
||||||
|
|
||||||
watch(() => previewApi.vmName, (name: string) => {
|
watch(previewApi, (api: Api) => {
|
||||||
if (name !== "") {
|
JGConsole.instance.updateConletTitle(conletId,
|
||||||
JGConsole.instance.updateConletTitle(conletId, name);
|
api.poolName || api.vmDefinition.name || "");
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
provideApi(previewDom, previewApi);
|
provideApi(previewDom, previewApi);
|
||||||
|
|
@ -88,16 +99,16 @@ window.orgJDrupesVmOperatorVmAccess.initPreview = (previewDom: HTMLElement,
|
||||||
JGConsole.notifyConletModel(conletId, action);
|
JGConsole.notifyConletModel(conletId, action);
|
||||||
};
|
};
|
||||||
|
|
||||||
return { localize, resourceBase, vmAction, configured,
|
return { localize, resourceBase, vmAction, poolName, vmName,
|
||||||
startable, stoppable, running, inUse, permissions };
|
configured, busy, startable, stoppable, running, inUse,
|
||||||
|
permissions };
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="2" style="position: relative"><span
|
<td rowspan="2" style="position: relative"><span
|
||||||
style="position: absolute;"
|
style="position: absolute;" :class="{ busy: busy }"
|
||||||
:class="{ busy: configured && !startable && !stoppable }"
|
|
||||||
><img role=button :aria-disabled="!running
|
><img role=button :aria-disabled="!running
|
||||||
|| !permissions.includes('accessConsole')"
|
|| !permissions.includes('accessConsole')"
|
||||||
v-on:click="vmAction('openConsole')"
|
v-on:click="vmAction('openConsole')"
|
||||||
|
|
@ -107,9 +118,12 @@ window.orgJDrupesVmOperatorVmAccess.initPreview = (previewDom: HTMLElement,
|
||||||
:title="localize('Open console')"></span><span
|
:title="localize('Open console')"></span><span
|
||||||
style="visibility: hidden;"><img
|
style="visibility: hidden;"><img
|
||||||
:src="resourceBase + 'computer.svg'"></span></td>
|
: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">
|
<td class="jdrupes-vmoperator-vmaccess-preview-action-list">
|
||||||
<span role="button"
|
<span role="button" :aria-disabled="!startable"
|
||||||
:aria-disabled="!startable || !permissions.includes('start')"
|
|
||||||
tabindex="0" class="fa fa-play" :title="localize('Start VM')"
|
tabindex="0" class="fa fa-play" :title="localize('Start VM')"
|
||||||
v-on:click="vmAction('start')"></span>
|
v-on:click="vmAction('start')"></span>
|
||||||
<span role="button"
|
<span role="button"
|
||||||
|
|
@ -127,9 +141,6 @@ window.orgJDrupesVmOperatorVmAccess.initPreview = (previewDom: HTMLElement,
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>`
|
</table>`
|
||||||
});
|
});
|
||||||
|
|
@ -139,24 +150,34 @@ window.orgJDrupesVmOperatorVmAccess.initPreview = (previewDom: HTMLElement,
|
||||||
};
|
};
|
||||||
|
|
||||||
JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmaccess.VmAccess",
|
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);
|
const conlet = JGConsole.findConletPreview(conletId);
|
||||||
if (!conlet) {
|
if (!conlet) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const api = getApi<Api>(conlet.element().querySelector(
|
const api = getApi<Api>(conlet.element().querySelector(
|
||||||
":scope .jdrupes-vmoperator-vmaccess-preview"))!;
|
":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",
|
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);
|
const conlet = JGConsole.findConletPreview(conletId);
|
||||||
if (!conlet) {
|
if (!conlet) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const api = getApi<Api>(conlet.element().querySelector(
|
const api = getApi<Api>(conlet.element().querySelector(
|
||||||
":scope .jdrupes-vmoperator-vmaccess-preview"))!;
|
":scope .jdrupes-vmoperator-vmaccess-preview"))!;
|
||||||
|
if (vmDefinition) {
|
||||||
// Add some short-cuts for rendering
|
// Add some short-cuts for rendering
|
||||||
vmDefinition.name = vmDefinition.metadata.name;
|
vmDefinition.name = vmDefinition.metadata.name;
|
||||||
vmDefinition.currentCpus = vmDefinition.status.cpus;
|
vmDefinition.currentCpus = vmDefinition.status.cpus;
|
||||||
|
|
@ -170,6 +191,9 @@ JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmaccess.VmAccess",
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
vmDefinition = {};
|
||||||
|
}
|
||||||
api.vmDefinition = vmDefinition;
|
api.vmDefinition = vmDefinition;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -203,19 +227,36 @@ window.orgJDrupesVmOperatorVmAccess.initEdit = (dialogDom: HTMLElement,
|
||||||
l10nBundles, JGWC.lang()!, key);
|
l10nBundles, JGWC.lang()!, key);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resource = ref<string>("vm");
|
||||||
const vmNameInput = ref<string>("");
|
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(
|
const conletId = (<HTMLElement>dialogDom.closest(
|
||||||
"[data-conlet-id]")!).dataset["conletId"]!;
|
"[data-conlet-id]")!).dataset["conletId"]!;
|
||||||
const conlet = JGConsole.findConletPreview(conletId);
|
const conlet = JGConsole.findConletPreview(conletId);
|
||||||
if (conlet) {
|
if (conlet) {
|
||||||
const api = getApi<Api>(conlet.element().querySelector(
|
const api = getApi<Api>(conlet.element().querySelector(
|
||||||
":scope .jdrupes-vmoperator-vmaccess-preview"))!;
|
":scope .jdrupes-vmoperator-vmaccess-preview"))!;
|
||||||
|
if (api.poolName) {
|
||||||
|
resource.value = "pool";
|
||||||
|
}
|
||||||
vmNameInput.value = api.vmName;
|
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);
|
app.use(JgwcPlugin);
|
||||||
|
|
@ -229,8 +270,9 @@ window.orgJDrupesVmOperatorVmAccess.applyEdit =
|
||||||
}
|
}
|
||||||
const conletId = (<HTMLElement>dialogDom.closest("[data-conlet-id]")!)
|
const conletId = (<HTMLElement>dialogDom.closest("[data-conlet-id]")!)
|
||||||
.dataset["conletId"]!;
|
.dataset["conletId"]!;
|
||||||
const vmName = getApi<ref<string>>(dialogDom!)!.value;
|
const editApi = getApi<ref<string>>(dialogDom!)!;
|
||||||
JGConsole.notifyConletModel(conletId, "selectedVm", vmName);
|
JGConsole.notifyConletModel(conletId, "selectedResource", editApi.resource(),
|
||||||
|
editApi.name());
|
||||||
}
|
}
|
||||||
|
|
||||||
window.orgJDrupesVmOperatorVmAccess.confirmReset =
|
window.orgJDrupesVmOperatorVmAccess.confirmReset =
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,12 @@
|
||||||
|
|
||||||
.jdrupes-vmoperator-vmaccess.jdrupes-vmoperator-vmaccess-preview {
|
.jdrupes-vmoperator-vmaccess.jdrupes-vmoperator-vmaccess-preview {
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
|
display: block;
|
||||||
height: 3em;
|
height: 3em;
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
|
|
||||||
|
|
@ -77,6 +82,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.jdrupes-vmoperator-vmaccess.jdrupes-vmoperator-vmaccess-edit {
|
.jdrupes-vmoperator-vmaccess.jdrupes-vmoperator-vmaccess-edit {
|
||||||
|
|
||||||
|
fieldset ul li {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
width: 15em;
|
width: 15em;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
:aria-expanded="(entry.name in detailsByName) ? 'true' : 'false'">
|
:aria-expanded="(entry.name in detailsByName) ? 'true' : 'false'">
|
||||||
<td v-for="key in controller.keys"
|
<td v-for="key in controller.keys"
|
||||||
v-bind:class="'column-' + key"
|
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">
|
v-bind:rowspan="(key == 'name') && $aash.isDisclosed(scopedId(rowIndex)) ? 2 : false">
|
||||||
<aash-disclosure-button v-if="key === 'name'" :type="'div'"
|
<aash-disclosure-button v-if="key === 'name'" :type="'div'"
|
||||||
:id-ref="scopedId(rowIndex)">
|
:id-ref="scopedId(rowIndex)">
|
||||||
|
|
@ -48,6 +48,11 @@
|
||||||
>{{ shortDateTime(entry[key].toString()) }}</span>
|
>{{ shortDateTime(entry[key].toString()) }}</span>
|
||||||
<span v-else-if="key === 'currentRam'"
|
<span v-else-if="key === 'currentRam'"
|
||||||
>{{ formatMemory(entry[key]) }}</span>
|
>{{ 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
|
<span v-else
|
||||||
v-html="controller.breakBeforeDots(entry[key])"></span>
|
v-html="controller.breakBeforeDots(entry[key])"></span>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -103,6 +108,12 @@
|
||||||
><span>{{ cic.error }}</span></form></td>
|
><span>{{ cic.error }}</span></form></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
<table class="table--basic table--basic--autoStriped">
|
||||||
|
<tr>
|
||||||
|
<td>{{ localize("usedFrom") }}</td>
|
||||||
|
<td>{{ entry.usedFrom }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,17 @@ conletName = VM Management
|
||||||
|
|
||||||
VMsSummary = VMs (running/total)
|
VMsSummary = VMs (running/total)
|
||||||
|
|
||||||
since = Since
|
assignedTo = Assigned to
|
||||||
currentCpus = Current CPUs
|
currentCpus = Current CPUs
|
||||||
currentRam = Current RAM
|
currentRam = Current RAM
|
||||||
maximumCpus = Maximum CPUs
|
maximumCpus = Maximum CPUs
|
||||||
maximumRam = Maximum RAM
|
maximumRam = Maximum RAM
|
||||||
|
notInUse = Currently closed
|
||||||
nodeName = Node
|
nodeName = Node
|
||||||
requestedCpus = Requested CPUs
|
requestedCpus = Requested CPUs
|
||||||
requestedRam = Requested RAM
|
requestedRam = Requested RAM
|
||||||
running = Running
|
running = Running
|
||||||
|
since = Since
|
||||||
usedBy = Used by
|
usedBy = Used by
|
||||||
usedFrom = Used from
|
usedFrom = Used from
|
||||||
vmActions = Actions
|
vmActions = Actions
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,17 @@ Period = Zeitraum
|
||||||
Last\ hour = Letzte Stunde
|
Last\ hour = Letzte Stunde
|
||||||
Last\ day = Letzter Tag
|
Last\ day = Letzter Tag
|
||||||
|
|
||||||
running = Gestartet
|
assignedTo = Zugewiesen an
|
||||||
since = Seit
|
|
||||||
currentCpus = Aktuelle CPUs
|
currentCpus = Aktuelle CPUs
|
||||||
currentRam = Akuelles RAM
|
currentRam = Akuelles RAM
|
||||||
maximumCpus = Maximale CPUs
|
maximumCpus = Maximale CPUs
|
||||||
maximumRam = Maximales RAM
|
maximumRam = Maximales RAM
|
||||||
nodeName = Knoten
|
nodeName = Knoten
|
||||||
|
notInUse = Derzeit geschlossen
|
||||||
requestedCpus = Angeforderte CPUs
|
requestedCpus = Angeforderte CPUs
|
||||||
requestedRam = Angefordertes RAM
|
requestedRam = Angefordertes RAM
|
||||||
|
running = Gestartet
|
||||||
|
since = Seit
|
||||||
usedBy = Benutzt durch
|
usedBy = Benutzt durch
|
||||||
usedFrom = Benutzt von
|
usedFrom = Benutzt von
|
||||||
vmActions = Aktionen
|
vmActions = Aktionen
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,7 @@ import java.math.BigDecimal;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
@ -328,14 +326,9 @@ public class VmMgmt extends FreeMarkerConlet<VmMgmt.VmsModel> {
|
||||||
.add(vmDef.<String> fromStatus("ram")
|
.add(vmDef.<String> fromStatus("ram")
|
||||||
.map(r -> Quantity.fromString(r).getNumber().toBigInteger())
|
.map(r -> Quantity.fromString(r).getNumber().toBigInteger())
|
||||||
.orElse(BigInteger.ZERO));
|
.orElse(BigInteger.ZERO));
|
||||||
summary.runningVms
|
if (vmDef.conditionStatus("Running").orElse(false)) {
|
||||||
+= vmDef.<List<Map<String, Object>>> fromStatus("conditions")
|
summary.runningVms += 1;
|
||||||
.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();
|
|
||||||
}
|
}
|
||||||
cachedSummary = summary;
|
cachedSummary = summary;
|
||||||
return summary;
|
return summary;
|
||||||
|
|
|
||||||
|
|
@ -75,17 +75,17 @@ window.orgJDrupesVmOperatorVmMgmt.initPreview = (previewDom: HTMLElement,
|
||||||
chart = new CpuRamChart(canvas, chartData);
|
chart = new CpuRamChart(canvas, chartData);
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(chartDateUpdate, (_) => {
|
watch(chartDateUpdate, (_: never) => {
|
||||||
chart?.update();
|
chart?.update();
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(JGWC.langRef(), (_) => {
|
watch(JGWC.langRef(), (_: never) => {
|
||||||
chart?.localizeChart();
|
chart?.localizeChart();
|
||||||
})
|
})
|
||||||
|
|
||||||
const period: Ref<string> = ref<string>("day");
|
const period: Ref<string> = ref<string>("day");
|
||||||
|
|
||||||
watch(period, (_) => {
|
watch(period, (_: never) => {
|
||||||
const hours = (period.value === "day") ? 24 : 1;
|
const hours = (period.value === "day") ? 24 : 1;
|
||||||
chart?.setPeriod(hours * 3600 * 1000);
|
chart?.setPeriod(hours * 3600 * 1000);
|
||||||
});
|
});
|
||||||
|
|
@ -112,8 +112,8 @@ window.orgJDrupesVmOperatorVmMgmt.initView = (viewDom: HTMLElement,
|
||||||
["currentCpus", "currentCpus"],
|
["currentCpus", "currentCpus"],
|
||||||
["currentRam", "currentRam"],
|
["currentRam", "currentRam"],
|
||||||
["nodeName", "nodeName"],
|
["nodeName", "nodeName"],
|
||||||
["usedFrom", "usedFrom"],
|
["usedBy", "usedBy"],
|
||||||
["usedBy", "usedBy"]
|
["assignedTo", "assignedTo"]
|
||||||
], {
|
], {
|
||||||
sortKey: "name",
|
sortKey: "name",
|
||||||
sortOrder: "up"
|
sortOrder: "up"
|
||||||
|
|
@ -183,6 +183,7 @@ JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmmgmt.VmMgmt",
|
||||||
vmDefinition.currentRam = Number(vmDefinition.status.ram);
|
vmDefinition.currentRam = Number(vmDefinition.status.ram);
|
||||||
vmDefinition.usedFrom = vmDefinition.status.consoleClient || "";
|
vmDefinition.usedFrom = vmDefinition.status.consoleClient || "";
|
||||||
vmDefinition.usedBy = vmDefinition.status.consoleUser || "";
|
vmDefinition.usedBy = vmDefinition.status.consoleUser || "";
|
||||||
|
vmDefinition.assignedTo = vmDefinition.status.assignment?.user || "";
|
||||||
for (const condition of vmDefinition.status.conditions) {
|
for (const condition of vmDefinition.status.conditions) {
|
||||||
if (condition.type === "Running") {
|
if (condition.type === "Running") {
|
||||||
vmDefinition.running = condition.status === "True";
|
vmDefinition.running = condition.status === "True";
|
||||||
|
|
|
||||||
|
|
@ -72,12 +72,18 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.console-conection-closed {
|
||||||
|
color: var(--disabled);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
td.details {
|
td.details {
|
||||||
padding-left: 1em;
|
padding-left: 0;
|
||||||
|
|
||||||
table {
|
table {
|
||||||
|
display: inline;
|
||||||
|
|
||||||
td:nth-child(2) {
|
td:nth-child(2) {
|
||||||
min-width: 7em;
|
min-width: 7em;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue