Merge branch 'feature/auto-login' into testing

This commit is contained in:
Michael Lipp 2025-03-05 13:33:48 +01:00
commit ef3d680421
30 changed files with 460 additions and 286 deletions

38
.markdownlint.yaml Normal file
View 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

View file

@ -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: >-

View file

@ -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

View file

@ -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:

View file

@ -8,12 +8,11 @@ metadata:
spec: spec:
image: image:
# repository: docker-registry.lan.mnl.de
# path: vmoperator/org.jdrupes.vmoperator.runner.qemu-arch
# pullPolicy: Always # pullPolicy: Always
# repository: ghcr.io # repository: ghcr.io
# path: mnlipp/org.jdrupes.vmoperator.runner.qemu-alpine # path: mnlipp/org.jdrupes.vmoperator.runner.qemu-alpine
# version: "3.0.0" # version: "3.0.0"
# source: docker-registry.lan.mnl.de/vm-operator/org.jdrupes.vmoperator.runner.qemu-arch:feature-auto-login
source: registry.mnl.de/org/jdrupes/vm-operator/org.jdrupes.vmoperator.runner.qemu-arch:testing source: registry.mnl.de/org/jdrupes/vm-operator/org.jdrupes.vmoperator.runner.qemu-arch:testing
pullPolicy: Always pullPolicy: Always

View file

@ -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";
}
}
} }
/** /**

View file

@ -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));
} }
/** /**

View file

@ -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;
} }

View file

@ -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.
*
* @param vmDef the vm name
* @param user the requesting user
* @param loginUser login the user
*/
public PrepareConsole(VmDefinition vmDef, String user,
boolean loginUser) {
this.vmDef = vmDef;
this.user = user;
this.loginUser = loginUser;
}
/**
* Instantiates a new request for the display secret.
* After handling the event, a result of `null` means that
* no password 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
*/ */
public PrepareConsole(VmDefinition vmDef, String user) { public GetDisplaySecret(VmDefinition vmDef, String user) {
this(vmDef, user, false); this.vmDef = vmDef;
this.user = user;
} }
/** /**
* Gets the vm definition. * Gets the VM definition.
* *
* @return 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.");
} }

View file

@ -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
*/ */

View file

@ -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:

View file

@ -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;

View file

@ -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;

View file

@ -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;
} }

View file

@ -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

View file

@ -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));

View file

@ -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;
}); });
} }

View file

@ -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;
} }

View file

@ -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.

View file

@ -4,4 +4,4 @@
<rulesets> <rulesets>
<ruleset name="Custom Rules" ref="VM-Operator/ruleset.xml" refcontext="workspace" /> <ruleset name="Custom Rules" ref="VM-Operator/ruleset.xml" refcontext="workspace" />
</rulesets> </rulesets>
</eclipse-pmd> </eclipse-pmd>

View file

@ -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.

View file

@ -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.

View file

@ -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)));

View file

@ -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 {

View file

@ -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);

View file

@ -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>

View file

@ -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)));

View file

@ -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);

View file

@ -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,18 +44,18 @@ 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
The VM-Operator agent runs as a systemd service. Sample configuration The VM-Operator agent runs as a systemd service. Sample configuration
files can be found files can be found
[here](https://github.com/mnlipp/VM-Operator/tree/main/dev-example/vmop-agent). [here](https://github.com/mnlipp/VM-Operator/tree/main/dev-example/vmop-agent).
Copy Copy
* `99-vmop-agent.rules` to `/usr/local/lib/udev/rules.d/99-vmop-agent.rules`, * `99-vmop-agent.rules` to `/usr/local/lib/udev/rules.d/99-vmop-agent.rules`,
* `vmop-agent` to `/usr/local/libexec/vmop-agent` and * `vmop-agent` to `/usr/local/libexec/vmop-agent` and

View file

@ -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
`/Manager/Controller/DisplaySecretMonitor` to
`/Manager/Controller/Reconciler/DisplaySecretReconciler`. The old path is
still accepted for backward compatibility until the next major version,
but should be updated.
* The standard [template](./runner.html#stand-alone-configuration) used * The configuration property `passwordValidity` has been moved from component
to generate the QEMU command has been updated. Unless you have enabled `/Manager/Controller/DisplaySecretMonitor` to
automatic updates of the template in the VM definition, you have to `/Manager/Controller/Reconciler/DisplaySecretReconciler`. The old path is
update the template manually. If you're using your own template, you still accepted for backward compatibility until the next major version,
have to add a virtual serial port (see the git history of the standard but should be updated.
template for the required addition).
* The standard [template](./runner.html#stand-alone-configuration) used
to generate the QEMU command has been updated. Unless you have enabled
automatic updates of the template in the VM definition, you have to
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
template for the required addition).
## To version 3.4.0 ## To version 3.4.0