Merge branch 'main' into testing
This commit is contained in:
commit
93a1a2b2f9
15 changed files with 479 additions and 211 deletions
|
|
@ -1467,6 +1467,12 @@ spec:
|
||||||
The hostname of the currently connected client.
|
The hostname of the currently connected client.
|
||||||
type: string
|
type: string
|
||||||
default: ""
|
default: ""
|
||||||
|
consoleUser:
|
||||||
|
description: >-
|
||||||
|
The id of the user who has last requested a console
|
||||||
|
connection.
|
||||||
|
type: string
|
||||||
|
default: ""
|
||||||
displayPasswordSerial:
|
displayPasswordSerial:
|
||||||
description: >-
|
description: >-
|
||||||
Counts changes of the display password. Set to -1
|
Counts changes of the display password. Set to -1
|
||||||
|
|
|
||||||
|
|
@ -193,7 +193,33 @@ public class K8sGenericStub<O extends KubernetesObject,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the object's status.
|
* Updates the object's status, retrying for the given number of times
|
||||||
|
* if the update fails due to a conflict.
|
||||||
|
*
|
||||||
|
* @param object the current state of the object (passed to `status`)
|
||||||
|
* @param status function that returns the new status
|
||||||
|
* @param retries the retries
|
||||||
|
* @return the updated model or empty if not successful
|
||||||
|
* @throws ApiException the api exception
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("PMD.AssignmentInOperand")
|
||||||
|
public Optional<O> updateStatus(O object,
|
||||||
|
Function<O, Object> status, int retries) throws ApiException {
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
return K8s.optional(api.updateStatus(object, status));
|
||||||
|
} catch (ApiException e) {
|
||||||
|
if (HttpURLConnection.HTTP_CONFLICT != e.getCode()
|
||||||
|
|| retries-- <= 0) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the object's status, retrying up to 16 times if there
|
||||||
|
* is a conflict.
|
||||||
*
|
*
|
||||||
* @param object the current state of the object (passed to `status`)
|
* @param object the current state of the object (passed to `status`)
|
||||||
* @param status function that returns the new status
|
* @param status function that returns the new status
|
||||||
|
|
@ -202,7 +228,7 @@ public class K8sGenericStub<O extends KubernetesObject,
|
||||||
*/
|
*/
|
||||||
public Optional<O> updateStatus(O object,
|
public Optional<O> updateStatus(O object,
|
||||||
Function<O, Object> status) throws ApiException {
|
Function<O, Object> status) throws ApiException {
|
||||||
return K8s.optional(api.updateStatus(object, status));
|
return updateStatus(object, status, 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -29,14 +29,17 @@ import org.jgrapes.core.Event;
|
||||||
public class GetDisplayPassword extends Event<String> {
|
public class GetDisplayPassword extends Event<String> {
|
||||||
|
|
||||||
private final VmDefinition vmDef;
|
private final VmDefinition vmDef;
|
||||||
|
private final String user;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a new returns the display secret.
|
* Instantiates a new request for the display secret.
|
||||||
*
|
*
|
||||||
* @param vmDef the vm name
|
* @param vmDef the vm name
|
||||||
|
* @param user the requesting user
|
||||||
*/
|
*/
|
||||||
public GetDisplayPassword(VmDefinition vmDef) {
|
public GetDisplayPassword(VmDefinition vmDef, String user) {
|
||||||
this.vmDef = vmDef;
|
this.vmDef = vmDef;
|
||||||
|
this.user = user;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -48,6 +51,15 @@ public class GetDisplayPassword extends Event<String> {
|
||||||
return vmDef;
|
return vmDef;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the id of the user who has requested the password.
|
||||||
|
*
|
||||||
|
* @return the string
|
||||||
|
*/
|
||||||
|
public String user() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the password. May only be called when the event is completed.
|
* Return the password. May only be called when the event is completed.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@
|
||||||
|
|
||||||
package org.jdrupes.vmoperator.manager;
|
package org.jdrupes.vmoperator.manager;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import io.kubernetes.client.apimachinery.GroupVersionKind;
|
||||||
import io.kubernetes.client.custom.V1Patch;
|
import io.kubernetes.client.custom.V1Patch;
|
||||||
import io.kubernetes.client.openapi.ApiException;
|
import io.kubernetes.client.openapi.ApiException;
|
||||||
import io.kubernetes.client.openapi.models.V1Secret;
|
import io.kubernetes.client.openapi.models.V1Secret;
|
||||||
|
|
@ -37,10 +39,13 @@ import java.util.Optional;
|
||||||
import java.util.Scanner;
|
import java.util.Scanner;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
|
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
|
||||||
|
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
|
||||||
|
import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
|
||||||
import static org.jdrupes.vmoperator.common.Constants.VM_OP_NAME;
|
import static org.jdrupes.vmoperator.common.Constants.VM_OP_NAME;
|
||||||
import org.jdrupes.vmoperator.common.K8sClient;
|
import org.jdrupes.vmoperator.common.K8sClient;
|
||||||
import org.jdrupes.vmoperator.common.K8sV1PodStub;
|
import org.jdrupes.vmoperator.common.K8sV1PodStub;
|
||||||
import org.jdrupes.vmoperator.common.K8sV1SecretStub;
|
import org.jdrupes.vmoperator.common.K8sV1SecretStub;
|
||||||
|
import org.jdrupes.vmoperator.common.VmDefinitionStub;
|
||||||
import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET;
|
import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET;
|
||||||
import static org.jdrupes.vmoperator.manager.Constants.DATA_DISPLAY_PASSWORD;
|
import static org.jdrupes.vmoperator.manager.Constants.DATA_DISPLAY_PASSWORD;
|
||||||
import static org.jdrupes.vmoperator.manager.Constants.DATA_PASSWORD_EXPIRY;
|
import static org.jdrupes.vmoperator.manager.Constants.DATA_PASSWORD_EXPIRY;
|
||||||
|
|
@ -181,12 +186,22 @@ public class DisplaySecretMonitor
|
||||||
+ "app.kubernetes.io/instance="
|
+ "app.kubernetes.io/instance="
|
||||||
+ event.vmDefinition().metadata().getName());
|
+ event.vmDefinition().metadata().getName());
|
||||||
var stubs = K8sV1SecretStub.list(client(),
|
var stubs = K8sV1SecretStub.list(client(),
|
||||||
event.vmDefinition().metadata().getNamespace(), options);
|
event.vmDefinition().namespace(), options);
|
||||||
if (stubs.isEmpty()) {
|
if (stubs.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var stub = stubs.iterator().next();
|
var stub = stubs.iterator().next();
|
||||||
|
|
||||||
|
// Valid request, update console user in status
|
||||||
|
var vmStub = VmDefinitionStub.get(client(),
|
||||||
|
new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM),
|
||||||
|
event.vmDefinition().namespace(), event.vmDefinition().name());
|
||||||
|
vmStub.updateStatus(from -> {
|
||||||
|
JsonObject status = from.status();
|
||||||
|
status.addProperty("consoleUser", event.user());
|
||||||
|
return status;
|
||||||
|
});
|
||||||
|
|
||||||
// Check validity
|
// Check validity
|
||||||
var model = stub.model().get();
|
var model = stub.model().get();
|
||||||
@SuppressWarnings("PMD.StringInstantiation")
|
@SuppressWarnings("PMD.StringInstantiation")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
/*
|
||||||
|
* VM-Operator
|
||||||
|
* Copyright (C) 2024 Michael N. Lipp
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jdrupes.vmoperator.runner.qemu;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import io.kubernetes.client.apimachinery.GroupVersionKind;
|
||||||
|
import io.kubernetes.client.openapi.ApiException;
|
||||||
|
import io.kubernetes.client.openapi.models.EventsV1Event;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
|
||||||
|
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
|
||||||
|
import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
|
||||||
|
import org.jdrupes.vmoperator.common.K8s;
|
||||||
|
import org.jdrupes.vmoperator.common.K8sClient;
|
||||||
|
import org.jdrupes.vmoperator.common.VmDefinitionStub;
|
||||||
|
import org.jdrupes.vmoperator.runner.qemu.events.Exit;
|
||||||
|
import org.jdrupes.vmoperator.runner.qemu.events.SpiceDisconnectedEvent;
|
||||||
|
import org.jdrupes.vmoperator.runner.qemu.events.SpiceInitializedEvent;
|
||||||
|
import org.jgrapes.core.Channel;
|
||||||
|
import org.jgrapes.core.annotation.Handler;
|
||||||
|
import org.jgrapes.core.events.Start;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A (sub)component that updates the console status in the CR status.
|
||||||
|
* Created as child of {@link StatusUpdater}.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
||||||
|
public class ConsoleTracker extends VmDefUpdater {
|
||||||
|
|
||||||
|
private VmDefinitionStub vmStub;
|
||||||
|
private String mainChannelClientHost;
|
||||||
|
private long mainChannelClientPort;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new status updater.
|
||||||
|
*
|
||||||
|
* @param componentChannel the component channel
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
|
||||||
|
public ConsoleTracker(Channel componentChannel) {
|
||||||
|
super(componentChannel);
|
||||||
|
apiClient = (K8sClient) io.kubernetes.client.openapi.Configuration
|
||||||
|
.getDefaultApiClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the start event.
|
||||||
|
*
|
||||||
|
* @param event the event
|
||||||
|
* @throws IOException
|
||||||
|
* @throws ApiException
|
||||||
|
*/
|
||||||
|
@Handler
|
||||||
|
public void onStart(Start event) {
|
||||||
|
if (namespace == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
vmStub = VmDefinitionStub.get(apiClient,
|
||||||
|
new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM),
|
||||||
|
namespace, vmName);
|
||||||
|
} catch (ApiException e) {
|
||||||
|
logger.log(Level.SEVERE, e,
|
||||||
|
() -> "Cannot access VM object, terminating.");
|
||||||
|
event.cancel(true);
|
||||||
|
fire(new Exit(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On spice connected.
|
||||||
|
*
|
||||||
|
* @param event the event
|
||||||
|
* @throws ApiException the api exception
|
||||||
|
*/
|
||||||
|
@Handler
|
||||||
|
@SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition",
|
||||||
|
"PMD.AvoidDuplicateLiterals" })
|
||||||
|
public void onSpiceInitialized(SpiceInitializedEvent event)
|
||||||
|
throws ApiException {
|
||||||
|
if (vmStub == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only process connections using main channel.
|
||||||
|
if (event.channelType() != 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mainChannelClientHost = event.clientHost();
|
||||||
|
mainChannelClientPort = event.clientPort();
|
||||||
|
vmStub.updateStatus(from -> {
|
||||||
|
JsonObject status = from.status();
|
||||||
|
status.addProperty("consoleClient", event.clientHost());
|
||||||
|
updateCondition(from, status, "ConsoleConnected", true, "Connected",
|
||||||
|
"Connection from " + event.clientHost());
|
||||||
|
return status;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log event
|
||||||
|
var evt = new EventsV1Event()
|
||||||
|
.reportingController(VM_OP_GROUP + "/" + APP_NAME)
|
||||||
|
.action("ConsoleConnectionUpdate")
|
||||||
|
.reason("Connection from " + event.clientHost());
|
||||||
|
K8s.createEvent(apiClient, vmStub.model().get(), evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On spice disconnected.
|
||||||
|
*
|
||||||
|
* @param event the event
|
||||||
|
* @throws ApiException the api exception
|
||||||
|
*/
|
||||||
|
@Handler
|
||||||
|
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
|
||||||
|
public void onSpiceDisconnected(SpiceDisconnectedEvent event)
|
||||||
|
throws ApiException {
|
||||||
|
if (vmStub == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only process disconnects from main channel.
|
||||||
|
if (!event.clientHost().equals(mainChannelClientHost)
|
||||||
|
|| event.clientPort() != mainChannelClientPort) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
vmStub.updateStatus(from -> {
|
||||||
|
JsonObject status = from.status();
|
||||||
|
status.addProperty("consoleClient", "");
|
||||||
|
updateCondition(from, status, "ConsoleConnected", false,
|
||||||
|
"Disconnected", event.clientHost() + " has disconnected");
|
||||||
|
return status;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log event
|
||||||
|
var evt = new EventsV1Event()
|
||||||
|
.reportingController(VM_OP_GROUP + "/" + APP_NAME)
|
||||||
|
.action("ConsoleConnectionUpdate")
|
||||||
|
.reason("Disconnected from " + event.clientHost());
|
||||||
|
K8s.createEvent(apiClient, vmStub.model().get(), evt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,22 +27,12 @@ import io.kubernetes.client.openapi.ApiException;
|
||||||
import io.kubernetes.client.openapi.models.EventsV1Event;
|
import io.kubernetes.client.openapi.models.EventsV1Event;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
|
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
|
||||||
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
|
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
|
||||||
import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
|
import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
|
||||||
import org.jdrupes.vmoperator.common.K8s;
|
import org.jdrupes.vmoperator.common.K8s;
|
||||||
import org.jdrupes.vmoperator.common.K8sClient;
|
|
||||||
import org.jdrupes.vmoperator.common.K8sDynamicModel;
|
|
||||||
import org.jdrupes.vmoperator.common.VmDefinitionModel;
|
import org.jdrupes.vmoperator.common.VmDefinitionModel;
|
||||||
import org.jdrupes.vmoperator.common.VmDefinitionStub;
|
import org.jdrupes.vmoperator.common.VmDefinitionStub;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.BalloonChangeEvent;
|
import org.jdrupes.vmoperator.runner.qemu.events.BalloonChangeEvent;
|
||||||
|
|
@ -53,29 +43,21 @@ import org.jdrupes.vmoperator.runner.qemu.events.HotpluggableCpuStatus;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange;
|
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState;
|
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.ShutdownEvent;
|
import org.jdrupes.vmoperator.runner.qemu.events.ShutdownEvent;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.SpiceConnectedEvent;
|
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.SpiceDisconnectedEvent;
|
|
||||||
import org.jdrupes.vmoperator.util.GsonPtr;
|
import org.jdrupes.vmoperator.util.GsonPtr;
|
||||||
import org.jgrapes.core.Channel;
|
import org.jgrapes.core.Channel;
|
||||||
import org.jgrapes.core.Component;
|
|
||||||
import org.jgrapes.core.annotation.Handler;
|
import org.jgrapes.core.annotation.Handler;
|
||||||
import org.jgrapes.core.events.HandlingError;
|
import org.jgrapes.core.events.HandlingError;
|
||||||
import org.jgrapes.core.events.Start;
|
import org.jgrapes.core.events.Start;
|
||||||
import org.jgrapes.util.events.ConfigurationUpdate;
|
|
||||||
import org.jgrapes.util.events.InitialConfiguration;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the CR status.
|
* Updates the CR status.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
||||||
public class StatusUpdater extends Component {
|
public class StatusUpdater extends VmDefUpdater {
|
||||||
|
|
||||||
private static final Set<RunState> RUNNING_STATES
|
private static final Set<RunState> RUNNING_STATES
|
||||||
= Set.of(RunState.RUNNING, RunState.TERMINATING);
|
= Set.of(RunState.RUNNING, RunState.TERMINATING);
|
||||||
|
|
||||||
private String namespace;
|
|
||||||
private String vmName;
|
|
||||||
private K8sClient apiClient;
|
|
||||||
private long observedGeneration;
|
private long observedGeneration;
|
||||||
private boolean guestShutdownStops;
|
private boolean guestShutdownStops;
|
||||||
private boolean shutdownByGuest;
|
private boolean shutdownByGuest;
|
||||||
|
|
@ -89,15 +71,7 @@ public class StatusUpdater extends Component {
|
||||||
@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
|
@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
|
||||||
public StatusUpdater(Channel componentChannel) {
|
public StatusUpdater(Channel componentChannel) {
|
||||||
super(componentChannel);
|
super(componentChannel);
|
||||||
try {
|
attach(new ConsoleTracker(componentChannel));
|
||||||
apiClient = new K8sClient();
|
|
||||||
io.kubernetes.client.openapi.Configuration
|
|
||||||
.setDefaultApiClient(apiClient);
|
|
||||||
} catch (IOException e) {
|
|
||||||
logger.log(Level.SEVERE, e,
|
|
||||||
() -> "Cannot access events API, terminating.");
|
|
||||||
fire(new Exit(1));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -114,43 +88,6 @@ public class StatusUpdater extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* On configuration update.
|
|
||||||
*
|
|
||||||
* @param event the event
|
|
||||||
*/
|
|
||||||
@Handler
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public void onConfigurationUpdate(ConfigurationUpdate event) {
|
|
||||||
event.structured("/Runner").ifPresent(c -> {
|
|
||||||
if (event instanceof InitialConfiguration) {
|
|
||||||
namespace = (String) c.get("namespace");
|
|
||||||
updateNamespace();
|
|
||||||
vmName = Optional.ofNullable((Map<String, String>) c.get("vm"))
|
|
||||||
.map(vm -> vm.get("name")).orElse(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateNamespace() {
|
|
||||||
if (namespace == null) {
|
|
||||||
var path = Path
|
|
||||||
.of("/var/run/secrets/kubernetes.io/serviceaccount/namespace");
|
|
||||||
if (Files.isReadable(path)) {
|
|
||||||
try {
|
|
||||||
namespace = Files.lines(path).findFirst().orElse(null);
|
|
||||||
} catch (IOException e) {
|
|
||||||
logger.log(Level.WARNING, e,
|
|
||||||
() -> "Cannot read namespace.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (namespace == null) {
|
|
||||||
logger.warning(() -> "Namespace is unknown, some functions"
|
|
||||||
+ " won't be available.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle the start event.
|
* Handle the start event.
|
||||||
*
|
*
|
||||||
|
|
@ -238,13 +175,9 @@ public class StatusUpdater extends Component {
|
||||||
}
|
}
|
||||||
vmStub.updateStatus(vmDef, from -> {
|
vmStub.updateStatus(vmDef, from -> {
|
||||||
JsonObject status = from.status();
|
JsonObject status = from.status();
|
||||||
status.getAsJsonArray("conditions").asList().stream()
|
boolean running = RUNNING_STATES.contains(event.runState());
|
||||||
.map(cond -> (JsonObject) cond)
|
updateCondition(vmDef, vmDef.status(), "Running", running,
|
||||||
.forEach(cond -> {
|
event.reason(), event.message());
|
||||||
if ("Running".equals(cond.get("type").getAsString())) {
|
|
||||||
updateRunningCondition(event, from, cond);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (event.runState() == RunState.STARTING) {
|
if (event.runState() == RunState.STARTING) {
|
||||||
status.addProperty("ram", GsonPtr.to(from.data())
|
status.addProperty("ram", GsonPtr.to(from.data())
|
||||||
.getAsString("spec", "vm", "maximumRam").orElse("0"));
|
.getAsString("spec", "vm", "maximumRam").orElse("0"));
|
||||||
|
|
@ -253,6 +186,13 @@ public class StatusUpdater extends Component {
|
||||||
status.addProperty("ram", "0");
|
status.addProperty("ram", "0");
|
||||||
status.addProperty("cpus", 0);
|
status.addProperty("cpus", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In case console connection was still present
|
||||||
|
if (!running) {
|
||||||
|
status.addProperty("consoleClient", "");
|
||||||
|
updateCondition(from, status, "ConsoleConnected", false,
|
||||||
|
"VmStopped", "The VM has been shut down");
|
||||||
|
}
|
||||||
return status;
|
return status;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -278,29 +218,6 @@ public class StatusUpdater extends Component {
|
||||||
K8s.createEvent(apiClient, vmDef, evt);
|
K8s.createEvent(apiClient, vmDef, evt);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateRunningCondition(RunnerStateChange event,
|
|
||||||
K8sDynamicModel from, JsonObject cond) {
|
|
||||||
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
|
|
||||||
boolean reportedRunning
|
|
||||||
= "True".equals(cond.get("status").getAsString());
|
|
||||||
if (RUNNING_STATES.contains(event.runState())
|
|
||||||
&& !reportedRunning) {
|
|
||||||
cond.addProperty("status", "True");
|
|
||||||
cond.addProperty("lastTransitionTime",
|
|
||||||
Instant.now().toString());
|
|
||||||
}
|
|
||||||
if (!RUNNING_STATES.contains(event.runState())
|
|
||||||
&& reportedRunning) {
|
|
||||||
cond.addProperty("status", "False");
|
|
||||||
cond.addProperty("lastTransitionTime",
|
|
||||||
Instant.now().toString());
|
|
||||||
}
|
|
||||||
cond.addProperty("reason", event.reason());
|
|
||||||
cond.addProperty("message", event.message());
|
|
||||||
cond.addProperty("observedGeneration",
|
|
||||||
from.getMetadata().getGeneration());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* On ballon change.
|
* On ballon change.
|
||||||
*
|
*
|
||||||
|
|
@ -369,91 +286,4 @@ public class StatusUpdater extends Component {
|
||||||
public void onShutdown(ShutdownEvent event) throws ApiException {
|
public void onShutdown(ShutdownEvent event) throws ApiException {
|
||||||
shutdownByGuest = event.byGuest();
|
shutdownByGuest = event.byGuest();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* On spice connected.
|
|
||||||
*
|
|
||||||
* @param event the event
|
|
||||||
* @throws ApiException the api exception
|
|
||||||
*/
|
|
||||||
@Handler
|
|
||||||
public void onSpiceConnected(SpiceConnectedEvent event)
|
|
||||||
throws ApiException {
|
|
||||||
if (vmStub == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
vmStub.updateStatus(from -> {
|
|
||||||
JsonObject status = from.status();
|
|
||||||
status.addProperty("consoleClient", event.clientHost());
|
|
||||||
updateConsoleConnectedCondition(from, status, true);
|
|
||||||
return status;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Log event
|
|
||||||
var evt = new EventsV1Event()
|
|
||||||
.reportingController(VM_OP_GROUP + "/" + APP_NAME)
|
|
||||||
.action("ConsoleConnectionUpdate")
|
|
||||||
.reason("Connection from " + event.clientHost());
|
|
||||||
K8s.createEvent(apiClient, vmStub.model().get(), evt);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* On spice disconnected.
|
|
||||||
*
|
|
||||||
* @param event the event
|
|
||||||
* @throws ApiException the api exception
|
|
||||||
*/
|
|
||||||
@Handler
|
|
||||||
public void onSpiceDisconnected(SpiceDisconnectedEvent event)
|
|
||||||
throws ApiException {
|
|
||||||
if (vmStub == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
vmStub.updateStatus(from -> {
|
|
||||||
JsonObject status = from.status();
|
|
||||||
status.addProperty("consoleClient", "");
|
|
||||||
updateConsoleConnectedCondition(from, status, false);
|
|
||||||
return status;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Log event
|
|
||||||
var evt = new EventsV1Event()
|
|
||||||
.reportingController(VM_OP_GROUP + "/" + APP_NAME)
|
|
||||||
.action("ConsoleConnectionUpdate")
|
|
||||||
.reason("Disconnected from " + event.clientHost());
|
|
||||||
K8s.createEvent(apiClient, vmStub.model().get(), evt);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateConsoleConnectedCondition(VmDefinitionModel from,
|
|
||||||
JsonObject status, boolean connected) {
|
|
||||||
// Optimize, as we can get this several times
|
|
||||||
var current = status.getAsJsonArray("conditions").asList().stream()
|
|
||||||
.map(cond -> (JsonObject) cond)
|
|
||||||
.filter(cond -> "ConsoleConnected"
|
|
||||||
.equals(cond.get("type").getAsString()))
|
|
||||||
.findFirst()
|
|
||||||
.map(cond -> "True".equals(cond.get("status").getAsString()));
|
|
||||||
if (current.isPresent() && current.get() == connected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do update
|
|
||||||
final var condition = Map.of("type", "ConsoleConnected",
|
|
||||||
"status", connected ? "True" : "False",
|
|
||||||
"observedGeneration", from.getMetadata().getGeneration(),
|
|
||||||
"reason", connected ? "Connected" : "Disconnected",
|
|
||||||
"lastTransitionTime", Instant.now().toString());
|
|
||||||
List<Object> toReplace = new ArrayList<>(List.of(condition));
|
|
||||||
List<Object> newConds
|
|
||||||
= status.getAsJsonArray("conditions").asList().stream()
|
|
||||||
.map(cond -> (JsonObject) cond)
|
|
||||||
.map(cond -> "ConsoleConnected"
|
|
||||||
.equals(cond.get("type").getAsString())
|
|
||||||
? toReplace.remove(0)
|
|
||||||
: cond)
|
|
||||||
.collect(Collectors.toCollection(() -> new ArrayList<>()));
|
|
||||||
newConds.addAll(toReplace);
|
|
||||||
status.add("conditions",
|
|
||||||
apiClient.getJSON().getGson().toJsonTree(newConds));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
/*
|
||||||
|
* VM-Operator
|
||||||
|
* Copyright (C) 2024 Michael N. Lipp
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jdrupes.vmoperator.runner.qemu;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import org.jdrupes.vmoperator.common.K8sClient;
|
||||||
|
import org.jdrupes.vmoperator.common.VmDefinitionModel;
|
||||||
|
import org.jdrupes.vmoperator.runner.qemu.events.Exit;
|
||||||
|
import org.jgrapes.core.Channel;
|
||||||
|
import org.jgrapes.core.Component;
|
||||||
|
import org.jgrapes.core.annotation.Handler;
|
||||||
|
import org.jgrapes.util.events.ConfigurationUpdate;
|
||||||
|
import org.jgrapes.util.events.InitialConfiguration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the CR status.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
||||||
|
public class VmDefUpdater extends Component {
|
||||||
|
|
||||||
|
protected String namespace;
|
||||||
|
protected String vmName;
|
||||||
|
protected K8sClient apiClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new status updater.
|
||||||
|
*
|
||||||
|
* @param componentChannel the component channel
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
|
||||||
|
public VmDefUpdater(Channel componentChannel) {
|
||||||
|
super(componentChannel);
|
||||||
|
if (apiClient == null) {
|
||||||
|
try {
|
||||||
|
apiClient = new K8sClient();
|
||||||
|
io.kubernetes.client.openapi.Configuration
|
||||||
|
.setDefaultApiClient(apiClient);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.log(Level.SEVERE, e,
|
||||||
|
() -> "Cannot access events API, terminating.");
|
||||||
|
fire(new Exit(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On configuration update.
|
||||||
|
*
|
||||||
|
* @param event the event
|
||||||
|
*/
|
||||||
|
@Handler
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void onConfigurationUpdate(ConfigurationUpdate event) {
|
||||||
|
event.structured("/Runner").ifPresent(c -> {
|
||||||
|
if (event instanceof InitialConfiguration) {
|
||||||
|
namespace = (String) c.get("namespace");
|
||||||
|
updateNamespace();
|
||||||
|
vmName = Optional.ofNullable((Map<String, String>) c.get("vm"))
|
||||||
|
.map(vm -> vm.get("name")).orElse(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateNamespace() {
|
||||||
|
if (namespace == null) {
|
||||||
|
var path = Path
|
||||||
|
.of("/var/run/secrets/kubernetes.io/serviceaccount/namespace");
|
||||||
|
if (Files.isReadable(path)) {
|
||||||
|
try {
|
||||||
|
namespace = Files.lines(path).findFirst().orElse(null);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.log(Level.WARNING, e,
|
||||||
|
() -> "Cannot read namespace.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (namespace == null) {
|
||||||
|
logger.warning(() -> "Namespace is unknown, some functions"
|
||||||
|
+ " won't be available.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update condition.
|
||||||
|
*
|
||||||
|
* @param apiClient the api client
|
||||||
|
* @param from the vM definition
|
||||||
|
* @param status the current status
|
||||||
|
* @param type the condition type
|
||||||
|
* @param state the new state
|
||||||
|
* @param reason the reason for the change
|
||||||
|
*/
|
||||||
|
protected void updateCondition(VmDefinitionModel from, JsonObject status,
|
||||||
|
String type, boolean state, String reason, String message) {
|
||||||
|
// Optimize, as we can get this several times
|
||||||
|
var current = status.getAsJsonArray("conditions").asList().stream()
|
||||||
|
.map(cond -> (JsonObject) cond)
|
||||||
|
.filter(cond -> type.equals(cond.get("type").getAsString()))
|
||||||
|
.findFirst()
|
||||||
|
.map(cond -> "True".equals(cond.get("status").getAsString()));
|
||||||
|
if (current.isPresent() && current.get() == state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do update
|
||||||
|
final var condition = new HashMap<>(Map.of("type", type,
|
||||||
|
"status", state ? "True" : "False",
|
||||||
|
"observedGeneration", from.getMetadata().getGeneration(),
|
||||||
|
"reason", reason,
|
||||||
|
"lastTransitionTime", Instant.now().toString()));
|
||||||
|
if (message != null) {
|
||||||
|
condition.put("message", message);
|
||||||
|
}
|
||||||
|
List<Object> toReplace = new ArrayList<>(List.of(condition));
|
||||||
|
List<Object> newConds
|
||||||
|
= status.getAsJsonArray("conditions").asList().stream()
|
||||||
|
.map(cond -> (JsonObject) cond)
|
||||||
|
.map(cond -> type.equals(cond.get("type").getAsString())
|
||||||
|
? toReplace.remove(0)
|
||||||
|
: cond)
|
||||||
|
.collect(Collectors.toCollection(() -> new ArrayList<>()));
|
||||||
|
newConds.addAll(toReplace);
|
||||||
|
status.add("conditions",
|
||||||
|
apiClient.getJSON().getGson().toJsonTree(newConds));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -35,7 +35,7 @@ public class MonitorEvent extends Event<Void> {
|
||||||
*/
|
*/
|
||||||
public enum Kind {
|
public enum Kind {
|
||||||
READY, POWERDOWN, DEVICE_TRAY_MOVED, BALLOON_CHANGE, SHUTDOWN,
|
READY, POWERDOWN, DEVICE_TRAY_MOVED, BALLOON_CHANGE, SHUTDOWN,
|
||||||
SPICE_CONNECTED, SPICE_DISCONNECTED
|
SPICE_CONNECTED, SPICE_INITIALIZED, SPICE_DISCONNECTED
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Kind kind;
|
private final Kind kind;
|
||||||
|
|
@ -64,13 +64,14 @@ public class MonitorEvent extends Event<Void> {
|
||||||
return Optional
|
return Optional
|
||||||
.of(new ShutdownEvent(kind, response.get(EVENT_DATA)));
|
.of(new ShutdownEvent(kind, response.get(EVENT_DATA)));
|
||||||
case SPICE_CONNECTED:
|
case SPICE_CONNECTED:
|
||||||
return Optional
|
return Optional.of(new SpiceConnectedEvent(kind,
|
||||||
.of(new SpiceConnectedEvent(kind,
|
response.get(EVENT_DATA)));
|
||||||
response.get(EVENT_DATA)));
|
case SPICE_INITIALIZED:
|
||||||
|
return Optional.of(new SpiceInitializedEvent(kind,
|
||||||
|
response.get(EVENT_DATA)));
|
||||||
case SPICE_DISCONNECTED:
|
case SPICE_DISCONNECTED:
|
||||||
return Optional
|
return Optional.of(new SpiceDisconnectedEvent(kind,
|
||||||
.of(new SpiceDisconnectedEvent(kind,
|
response.get(EVENT_DATA)));
|
||||||
response.get(EVENT_DATA)));
|
|
||||||
default:
|
default:
|
||||||
return Optional
|
return Optional
|
||||||
.of(new MonitorEvent(kind, response.get(EVENT_DATA)));
|
.of(new MonitorEvent(kind, response.get(EVENT_DATA)));
|
||||||
|
|
|
||||||
|
|
@ -43,4 +43,13 @@ public class SpiceEvent extends MonitorEvent {
|
||||||
public String clientHost() {
|
public String clientHost() {
|
||||||
return data().get("client").get("host").asText();
|
return data().get("client").get("host").asText();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the client's port.
|
||||||
|
*
|
||||||
|
* @return the client's port number
|
||||||
|
*/
|
||||||
|
public long clientPort() {
|
||||||
|
return data().get("client").get("port").asLong();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* VM-Operator
|
||||||
|
* Copyright (C) 2023 Michael N. Lipp
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jdrupes.vmoperator.runner.qemu.events;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signals a connection from a client.
|
||||||
|
*/
|
||||||
|
public class SpiceInitializedEvent extends SpiceEvent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new spice connected event.
|
||||||
|
*
|
||||||
|
* @param kind the kind
|
||||||
|
* @param data the data
|
||||||
|
*/
|
||||||
|
public SpiceInitializedEvent(Kind kind, JsonNode data) {
|
||||||
|
super(kind, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the channel type.
|
||||||
|
*
|
||||||
|
* @return the channel type
|
||||||
|
*/
|
||||||
|
public int channelType() {
|
||||||
|
return data().get("client").get("channel-type").asInt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,5 +12,6 @@ requestedCpus = Requested CPUs
|
||||||
requestedRam = Requested RAM
|
requestedRam = Requested RAM
|
||||||
running = Running
|
running = Running
|
||||||
usedBy = Used by
|
usedBy = Used by
|
||||||
|
usedFrom = Used from
|
||||||
vmActions = Actions
|
vmActions = Actions
|
||||||
vmname = Name
|
vmname = Name
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ maximumRam = Maximales RAM
|
||||||
nodeName = Knoten
|
nodeName = Knoten
|
||||||
requestedCpus = Angeforderte CPUs
|
requestedCpus = Angeforderte CPUs
|
||||||
requestedRam = Angefordertes RAM
|
requestedRam = Angefordertes RAM
|
||||||
usedBy = Benutzt von
|
usedBy = Benutzt durch
|
||||||
|
usedFrom = Benutzt von
|
||||||
vmActions = Aktionen
|
vmActions = Aktionen
|
||||||
vmname = Name
|
vmname = Name
|
||||||
Value\ is\ above\ maximum = Wert ist zu groß
|
Value\ is\ above\ maximum = Wert ist zu groß
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,7 @@ window.orgJDrupesVmOperatorVmConlet.initView = (viewDom: HTMLElement,
|
||||||
["currentCpus", "currentCpus"],
|
["currentCpus", "currentCpus"],
|
||||||
["currentRam", "currentRam"],
|
["currentRam", "currentRam"],
|
||||||
["nodeName", "nodeName"],
|
["nodeName", "nodeName"],
|
||||||
|
["usedFrom", "usedFrom"],
|
||||||
["usedBy", "usedBy"]
|
["usedBy", "usedBy"]
|
||||||
], {
|
], {
|
||||||
sortKey: "name",
|
sortKey: "name",
|
||||||
|
|
@ -180,7 +181,8 @@ JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmconlet.VmConlet",
|
||||||
vmDefinition.name = vmDefinition.metadata.name;
|
vmDefinition.name = vmDefinition.metadata.name;
|
||||||
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.usedFrom = vmDefinition.status.consoleClient || "";
|
||||||
|
vmDefinition.usedBy = vmDefinition.status.consoleUser || "";
|
||||||
for (const condition of vmDefinition.status.conditions) {
|
for (const condition of vmDefinition.status.conditions) {
|
||||||
if (condition.type === "Running") {
|
if (condition.type === "Running") {
|
||||||
vmDefinition.running = condition.status === "True";
|
vmDefinition.running = condition.status === "True";
|
||||||
|
|
|
||||||
|
|
@ -51,9 +51,9 @@
|
||||||
inkscape:pageopacity="0.0"
|
inkscape:pageopacity="0.0"
|
||||||
inkscape:pagecheckerboard="0"
|
inkscape:pagecheckerboard="0"
|
||||||
inkscape:deskcolor="#d1d1d1"
|
inkscape:deskcolor="#d1d1d1"
|
||||||
inkscape:zoom="1.28"
|
inkscape:zoom="0.90509668"
|
||||||
inkscape:cx="326.5625"
|
inkscape:cx="345.81941"
|
||||||
inkscape:cy="548.04688"
|
inkscape:cy="376.2029"
|
||||||
inkscape:window-width="1920"
|
inkscape:window-width="1920"
|
||||||
inkscape:window-height="1008"
|
inkscape:window-height="1008"
|
||||||
inkscape:window-x="0"
|
inkscape:window-x="0"
|
||||||
|
|
@ -63,18 +63,17 @@
|
||||||
<path
|
<path
|
||||||
id="rect1"
|
id="rect1"
|
||||||
style="fill-opacity:0;stroke:#000000;stroke-width:1.97262;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:0;paint-order:fill markers stroke"
|
style="fill-opacity:0;stroke:#000000;stroke-width:1.97262;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:0;paint-order:fill markers stroke"
|
||||||
d="M 3.0038192,0.98808897 H 20.99618 V 13.006705 H 3.0038192 Z" />
|
d="m 4.7729709,13.006705 -1.7691517,0 V 0.98808897 H 20.99618 V 13.006705 l -1.639132,0"
|
||||||
<rect
|
sodipodi:nodetypes="cccccc" />
|
||||||
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.00306926;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
|
<path
|
||||||
id="rect2"
|
id="rect2"
|
||||||
width="23.995173"
|
style="opacity:1;stroke-width:0.00145614;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:0;stroke-dasharray:none;paint-order:fill markers stroke"
|
||||||
height="2.0017407"
|
d="m 0,13.998258 h 5.4336202 v 2.001741 H 0 Z"
|
||||||
x="0.0039473679"
|
sodipodi:nodetypes="ccccc" />
|
||||||
y="13.998839" />
|
|
||||||
<path
|
<path
|
||||||
id="rect3"
|
id="rect3"
|
||||||
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.05373;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
|
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1.05373;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
|
||||||
d="m 5.6839082,10.94394 c 0.2963594,-0.907428 2.9522319,-1.683971 2.767387,-1.6392261 0,0 1.5028596,1.6181771 3.6459428,1.6129171 2.018383,-0.005 3.362681,-1.6125503 3.362681,-1.6125503 -0.171441,-0.061235 2.778887,0.7741493 2.977303,1.6787203 0.393054,1.791919 0.25928,4.524228 0.25928,4.524228 L 5.3146272,15.391866 c 0,0 -0.181061,-2.762825 0.369281,-4.447926 z"
|
d="m 5.6839082,10.94394 c 0.2963594,-0.907428 2.9522319,-1.683971 2.767387,-1.6392261 0,0 1.5028596,1.6181771 3.6459428,1.6129171 2.018383,-0.005 3.362681,-1.6125503 3.362681,-1.6125503 -0.171441,-0.061235 2.778887,0.7741493 2.977303,1.6787203 0.393054,1.791919 0.25928,4.489072 0.25928,4.489072 l -13.3818748,0.001 c 0,0 -0.181061,-2.844856 0.369281,-4.529957 z"
|
||||||
sodipodi:nodetypes="sssssccs" />
|
sodipodi:nodetypes="sssssccs" />
|
||||||
<ellipse
|
<ellipse
|
||||||
style="fill:none;stroke:#000000;stroke-width:1.02152;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
|
style="fill:none;stroke:#000000;stroke-width:1.02152;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
|
||||||
|
|
@ -83,4 +82,9 @@
|
||||||
cy="6.3769712"
|
cy="6.3769712"
|
||||||
rx="3.2413731"
|
rx="3.2413731"
|
||||||
ry="3.225764" />
|
ry="3.225764" />
|
||||||
|
<path
|
||||||
|
id="rect2-2"
|
||||||
|
style="stroke-width:0.00145614;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:0;stroke-dasharray:none;paint-order:fill markers stroke"
|
||||||
|
d="M 18.56638,13.998258 H 24 v 2.001741 h -5.43362 z"
|
||||||
|
sodipodi:nodetypes="ccccc" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.6 KiB |
|
|
@ -527,9 +527,12 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
break;
|
break;
|
||||||
case "openConsole":
|
case "openConsole":
|
||||||
if (perms.contains(Permission.ACCESS_CONSOLE)) {
|
if (perms.contains(Permission.ACCESS_CONSOLE)) {
|
||||||
var pwQuery = Event.onCompletion(new GetDisplayPassword(vmDef),
|
var user = WebConsoleUtils.userFromSession(channel.session())
|
||||||
e -> openConsole(vmName, channel, model,
|
.map(ConsoleUser::getName).orElse("");
|
||||||
e.password().orElse(null)));
|
var pwQuery
|
||||||
|
= Event.onCompletion(new GetDisplayPassword(vmDef, user),
|
||||||
|
e -> openConsole(vmName, channel, model,
|
||||||
|
e.password().orElse(null)));
|
||||||
fire(pwQuery, vmChannel);
|
fire(pwQuery, vmChannel);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue