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