Add console access to VM management.
This commit is contained in:
parent
5cd4edcec1
commit
af41c78c07
14 changed files with 561 additions and 124 deletions
|
|
@ -75,6 +75,7 @@
|
||||||
admin:
|
admin:
|
||||||
- "*"
|
- "*"
|
||||||
operator:
|
operator:
|
||||||
|
- org.jdrupes.vmoperator.vmmgmt.VmMgmt
|
||||||
- org.jdrupes.vmoperator.vmaccess.VmAccess
|
- org.jdrupes.vmoperator.vmaccess.VmAccess
|
||||||
user:
|
user:
|
||||||
- org.jdrupes.vmoperator.vmaccess.VmAccess
|
- org.jdrupes.vmoperator.vmaccess.VmAccess
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* VM-Operator
|
* VM-Operator
|
||||||
* Copyright (C) 2024 Michael N. Lipp
|
* Copyright (C) 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
|
||||||
|
|
@ -22,11 +22,15 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
import io.kubernetes.client.openapi.models.V1Condition;
|
import io.kubernetes.client.openapi.models.V1Condition;
|
||||||
import io.kubernetes.client.openapi.models.V1ObjectMeta;
|
import io.kubernetes.client.openapi.models.V1ObjectMeta;
|
||||||
|
import io.kubernetes.client.util.Strings;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
@ -34,6 +38,8 @@ import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import org.jdrupes.vmoperator.util.DataPath;
|
import org.jdrupes.vmoperator.util.DataPath;
|
||||||
|
|
||||||
|
|
@ -43,7 +49,11 @@ import org.jdrupes.vmoperator.util.DataPath;
|
||||||
@SuppressWarnings({ "PMD.DataClass", "PMD.TooManyMethods" })
|
@SuppressWarnings({ "PMD.DataClass", "PMD.TooManyMethods" })
|
||||||
public class VmDefinition {
|
public class VmDefinition {
|
||||||
|
|
||||||
private static ObjectMapper objectMapper
|
@SuppressWarnings("PMD.FieldNamingConventions")
|
||||||
|
private static final Logger logger
|
||||||
|
= Logger.getLogger(VmDefinition.class.getName());
|
||||||
|
@SuppressWarnings("PMD.FieldNamingConventions")
|
||||||
|
private static final ObjectMapper objectMapper
|
||||||
= new ObjectMapper().registerModule(new JavaTimeModule());
|
= new ObjectMapper().registerModule(new JavaTimeModule());
|
||||||
|
|
||||||
private String kind;
|
private String kind;
|
||||||
|
|
@ -427,6 +437,8 @@ public class VmDefinition {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collect all permissions for the given user with the given roles.
|
* Collect all permissions for the given user with the given roles.
|
||||||
|
* If permission "takeConsole" is granted, the result will also
|
||||||
|
* contain "accessConsole" to simplify checks.
|
||||||
*
|
*
|
||||||
* @param user the user
|
* @param user the user
|
||||||
* @param roles the roles
|
* @param roles the roles
|
||||||
|
|
@ -434,7 +446,7 @@ public class VmDefinition {
|
||||||
*/
|
*/
|
||||||
public Set<Permission> permissionsFor(String user,
|
public Set<Permission> permissionsFor(String user,
|
||||||
Collection<String> roles) {
|
Collection<String> roles) {
|
||||||
return this.<List<Map<String, Object>>> fromSpec("permissions")
|
var result = this.<List<Map<String, Object>>> fromSpec("permissions")
|
||||||
.orElse(Collections.emptyList()).stream()
|
.orElse(Collections.emptyList()).stream()
|
||||||
.filter(p -> DataPath.get(p, "user").map(u -> u.equals(user))
|
.filter(p -> DataPath.get(p, "user").map(u -> u.equals(user))
|
||||||
.orElse(false)
|
.orElse(false)
|
||||||
|
|
@ -443,7 +455,29 @@ public class VmDefinition {
|
||||||
.orElse(Collections.emptyList()).stream())
|
.orElse(Collections.emptyList()).stream())
|
||||||
.flatMap(Function.identity())
|
.flatMap(Function.identity())
|
||||||
.map(Permission::parse).map(Set::stream)
|
.map(Permission::parse).map(Set::stream)
|
||||||
.flatMap(Function.identity()).collect(Collectors.toSet());
|
.flatMap(Function.identity())
|
||||||
|
.collect(Collectors.toCollection(HashSet::new));
|
||||||
|
|
||||||
|
// Take console implies access console, simplify checks
|
||||||
|
if (result.contains(Permission.TAKE_CONSOLE)) {
|
||||||
|
result.add(Permission.ACCESS_CONSOLE);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the console is accessible. Returns true if the console is
|
||||||
|
* currently unused, used by the given user or if the permissions
|
||||||
|
* allow taking over the console.
|
||||||
|
*
|
||||||
|
* @param user the user
|
||||||
|
* @param permissions the permissions
|
||||||
|
* @return true, if successful
|
||||||
|
*/
|
||||||
|
public boolean consoleAccessible(String user, Set<Permission> permissions) {
|
||||||
|
return !conditionStatus("ConsoleConnected").orElse(true)
|
||||||
|
|| consoleUser().map(cu -> cu.equals(user)).orElse(true)
|
||||||
|
|| permissions.contains(VmDefinition.Permission.TAKE_CONSOLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -456,6 +490,78 @@ public class VmDefinition {
|
||||||
.map(Number::longValue);
|
.map(Number::longValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a connection file.
|
||||||
|
*
|
||||||
|
* @param password the password
|
||||||
|
* @param preferredIpVersion the preferred IP version
|
||||||
|
* @param deleteConnectionFile the delete connection file
|
||||||
|
* @return the string
|
||||||
|
*/
|
||||||
|
public String connectionFile(String password,
|
||||||
|
Class<?> preferredIpVersion, boolean deleteConnectionFile) {
|
||||||
|
var addr = displayIp(preferredIpVersion);
|
||||||
|
if (addr.isEmpty()) {
|
||||||
|
logger.severe(() -> "Failed to find display IP for " + name());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var port = this.<Number> fromVm("display", "spice", "port")
|
||||||
|
.map(Number::longValue);
|
||||||
|
if (port.isEmpty()) {
|
||||||
|
logger.severe(() -> "No port defined for display of " + name());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
StringBuffer data = new StringBuffer(100)
|
||||||
|
.append("[virt-viewer]\ntype=spice\nhost=")
|
||||||
|
.append(addr.get().getHostAddress()).append("\nport=")
|
||||||
|
.append(port.get().toString())
|
||||||
|
.append('\n');
|
||||||
|
if (password != null) {
|
||||||
|
data.append("password=").append(password).append('\n');
|
||||||
|
}
|
||||||
|
this.<String> fromVm("display", "spice", "proxyUrl")
|
||||||
|
.ifPresent(u -> {
|
||||||
|
if (!Strings.isNullOrEmpty(u)) {
|
||||||
|
data.append("proxy=").append(u).append('\n');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (deleteConnectionFile) {
|
||||||
|
data.append("delete-this-file=1\n");
|
||||||
|
}
|
||||||
|
return data.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<InetAddress> displayIp(Class<?> preferredIpVersion) {
|
||||||
|
Optional<String> server = fromVm("display", "spice", "server");
|
||||||
|
if (server.isPresent()) {
|
||||||
|
var srv = server.get();
|
||||||
|
try {
|
||||||
|
var addr = InetAddress.getByName(srv);
|
||||||
|
logger.fine(() -> "Using IP address from CRD for "
|
||||||
|
+ getMetadata().getName() + ": " + addr);
|
||||||
|
return Optional.of(addr);
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
logger.log(Level.SEVERE, e, () -> "Invalid server address "
|
||||||
|
+ srv + ": " + e.getMessage());
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var addrs = Optional.<List<String>> ofNullable(
|
||||||
|
extra("nodeAddresses")).orElse(Collections.emptyList()).stream()
|
||||||
|
.map(a -> {
|
||||||
|
try {
|
||||||
|
return InetAddress.getByName(a);
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
logger.warning(() -> "Invalid IP address: " + a);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}).filter(a -> a != null).toList();
|
||||||
|
logger.fine(() -> "Known IP addresses for " + name() + ": " + addrs);
|
||||||
|
return addrs.stream()
|
||||||
|
.filter(a -> preferredIpVersion.isAssignableFrom(a.getClass()))
|
||||||
|
.findFirst().or(() -> addrs.stream().findFirst());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hash code.
|
* Hash code.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
|
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
|
||||||
|
|
@ -125,7 +126,12 @@ public class VmMonitor extends
|
||||||
protected void handleChange(K8sClient client,
|
protected void handleChange(K8sClient client,
|
||||||
Watch.Response<VmDefinitionModel> response) {
|
Watch.Response<VmDefinitionModel> response) {
|
||||||
V1ObjectMeta metadata = response.object.getMetadata();
|
V1ObjectMeta metadata = response.object.getMetadata();
|
||||||
VmChannel channel = channelManager.channelGet(metadata.getName());
|
AtomicBoolean toBeAdded = new AtomicBoolean(false);
|
||||||
|
VmChannel channel = channelManager.channel(metadata.getName())
|
||||||
|
.orElseGet(() -> {
|
||||||
|
toBeAdded.set(true);
|
||||||
|
return channelManager.createChannel(metadata.getName());
|
||||||
|
});
|
||||||
|
|
||||||
// Get full definition and associate with channel as backup
|
// Get full definition and associate with channel as backup
|
||||||
var vmModel = response.object;
|
var vmModel = response.object;
|
||||||
|
|
@ -151,6 +157,9 @@ public class VmMonitor extends
|
||||||
+ response.object.getMetadata());
|
+ response.object.getMetadata());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (toBeAdded.get()) {
|
||||||
|
channelManager.put(vmDef.name(), channel);
|
||||||
|
}
|
||||||
|
|
||||||
// Create and fire changed event. Remove channel from channel
|
// Create and fire changed event. Remove channel from channel
|
||||||
// manager on completion.
|
// manager on completion.
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,7 @@ import io.kubernetes.client.util.Strings;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.Inet4Address;
|
import java.net.Inet4Address;
|
||||||
import java.net.Inet6Address;
|
import java.net.Inet6Address;
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Base64;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
|
@ -779,24 +776,8 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "openConsole":
|
case "openConsole":
|
||||||
var user = WebConsoleUtils.userFromSession(channel.session())
|
if (perms.contains(VmDefinition.Permission.ACCESS_CONSOLE)) {
|
||||||
.map(ConsoleUser::getName).orElse("");
|
openConsole(channel, model, vmChannel, vmDef, perms);
|
||||||
if (vmDef.conditionStatus("ConsoleConnected").orElse(false)
|
|
||||||
&& vmDef.consoleUser().map(cu -> !cu.equals(user)
|
|
||||||
&& !perms.contains(VmDefinition.Permission.TAKE_CONSOLE))
|
|
||||||
.orElse(false)) {
|
|
||||||
channel.respond(new DisplayNotification(
|
|
||||||
resourceBundle.getString("consoleTakenNotification"),
|
|
||||||
Map.of("autoClose", 5_000, "type", "Warning")));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (perms.contains(VmDefinition.Permission.ACCESS_CONSOLE)
|
|
||||||
|| perms.contains(VmDefinition.Permission.TAKE_CONSOLE)) {
|
|
||||||
var pwQuery
|
|
||||||
= Event.onCompletion(new GetDisplayPassword(vmDef, user),
|
|
||||||
e -> openConsole(vmDef, channel, model,
|
|
||||||
e.password().orElse(null)));
|
|
||||||
fire(pwQuery, vmChannel);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:// ignore
|
default:// ignore
|
||||||
|
|
@ -804,6 +785,30 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void openConsole(ConsoleConnection channel, ResourceModel model,
|
||||||
|
VmChannel vmChannel, VmDefinition vmDef, Set<Permission> perms) {
|
||||||
|
var resourceBundle = resourceBundle(channel.locale());
|
||||||
|
var user = WebConsoleUtils.userFromSession(channel.session())
|
||||||
|
.map(ConsoleUser::getName).orElse("");
|
||||||
|
if (!vmDef.consoleAccessible(user, perms)) {
|
||||||
|
channel.respond(new DisplayNotification(
|
||||||
|
resourceBundle.getString("consoleTakenNotification"),
|
||||||
|
Map.of("autoClose", 5_000, "type", "Warning")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var pwQuery = Event.onCompletion(new GetDisplayPassword(vmDef, user),
|
||||||
|
e -> {
|
||||||
|
var data = vmDef.connectionFile(e.password().orElse(null),
|
||||||
|
preferredIpVersion, deleteConnectionFile);
|
||||||
|
if (data == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
channel.respond(new NotifyConletView(type(),
|
||||||
|
model.getConletId(), "openConsole", data));
|
||||||
|
});
|
||||||
|
fire(pwQuery, vmChannel);
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition",
|
@SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition",
|
||||||
"PMD.UseLocaleWithCaseConversions" })
|
"PMD.UseLocaleWithCaseConversions" })
|
||||||
private void selectResource(NotifyConletModel event,
|
private void selectResource(NotifyConletModel event,
|
||||||
|
|
@ -823,78 +828,6 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openConsole(VmDefinition vmDef, ConsoleConnection connection,
|
|
||||||
ResourceModel model, String password) {
|
|
||||||
if (vmDef == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var addr = displayIp(vmDef);
|
|
||||||
if (addr.isEmpty()) {
|
|
||||||
logger
|
|
||||||
.severe(() -> "Failed to find display IP for " + vmDef.name());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var port = vmDef.<Number> fromVm("display", "spice", "port")
|
|
||||||
.map(Number::longValue);
|
|
||||||
if (port.isEmpty()) {
|
|
||||||
logger
|
|
||||||
.severe(() -> "No port defined for display of " + vmDef.name());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
StringBuffer data = new StringBuffer(100)
|
|
||||||
.append("[virt-viewer]\ntype=spice\nhost=")
|
|
||||||
.append(addr.get().getHostAddress()).append("\nport=")
|
|
||||||
.append(port.get().toString())
|
|
||||||
.append('\n');
|
|
||||||
if (password != null) {
|
|
||||||
data.append("password=").append(password).append('\n');
|
|
||||||
}
|
|
||||||
vmDef.<String> fromVm("display", "spice", "proxyUrl")
|
|
||||||
.ifPresent(u -> {
|
|
||||||
if (!Strings.isNullOrEmpty(u)) {
|
|
||||||
data.append("proxy=").append(u).append('\n');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (deleteConnectionFile) {
|
|
||||||
data.append("delete-this-file=1\n");
|
|
||||||
}
|
|
||||||
connection.respond(new NotifyConletView(type(),
|
|
||||||
model.getConletId(), "openConsole", "application/x-virt-viewer",
|
|
||||||
Base64.getEncoder().encodeToString(data.toString().getBytes())));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<InetAddress> displayIp(VmDefinition vmDef) {
|
|
||||||
Optional<String> server = vmDef.fromVm("display", "spice", "server");
|
|
||||||
if (server.isPresent()) {
|
|
||||||
var srv = server.get();
|
|
||||||
try {
|
|
||||||
var addr = InetAddress.getByName(srv);
|
|
||||||
logger.fine(() -> "Using IP address from CRD for "
|
|
||||||
+ vmDef.getMetadata().getName() + ": " + addr);
|
|
||||||
return Optional.of(addr);
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
logger.log(Level.SEVERE, e, () -> "Invalid server address "
|
|
||||||
+ srv + ": " + e.getMessage());
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var addrs = Optional.<List<String>> ofNullable(vmDef
|
|
||||||
.extra("nodeAddresses")).orElse(Collections.emptyList()).stream()
|
|
||||||
.map(a -> {
|
|
||||||
try {
|
|
||||||
return InetAddress.getByName(a);
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
logger.warning(() -> "Invalid IP address: " + a);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}).filter(a -> a != null).toList();
|
|
||||||
logger.fine(() -> "Known IP addresses for "
|
|
||||||
+ vmDef.name() + ": " + addrs);
|
|
||||||
return addrs.stream()
|
|
||||||
.filter(a -> preferredIpVersion.isAssignableFrom(a.getClass()))
|
|
||||||
.findFirst().or(() -> addrs.stream().findFirst());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void confirmReset(NotifyConletModel event,
|
private void confirmReset(NotifyConletModel event,
|
||||||
ConsoleConnection channel, ResourceModel model,
|
ConsoleConnection channel, ResourceModel model,
|
||||||
ResourceBundle resourceBundle) throws TemplateNotFoundException,
|
ResourceBundle resourceBundle) throws TemplateNotFoundException,
|
||||||
|
|
|
||||||
|
|
@ -198,7 +198,7 @@ JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmaccess.VmAccess",
|
||||||
});
|
});
|
||||||
|
|
||||||
JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmaccess.VmAccess",
|
JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmaccess.VmAccess",
|
||||||
"openConsole", function(_conletId: string, mimeType: string, data: string) {
|
"openConsole", function(_conletId: string, data: string) {
|
||||||
let target = document.getElementById(
|
let target = document.getElementById(
|
||||||
"org.jdrupes.vmoperator.vmaccess.VmAccess.target");
|
"org.jdrupes.vmoperator.vmaccess.VmAccess.target");
|
||||||
if (!target) {
|
if (!target) {
|
||||||
|
|
@ -208,7 +208,8 @@ JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmaccess.VmAccess",
|
||||||
target.setAttribute("style", "display: none;");
|
target.setAttribute("style", "display: none;");
|
||||||
document.querySelector("body")!.append(target);
|
document.querySelector("body")!.append(target);
|
||||||
}
|
}
|
||||||
const url = "data:" + mimeType + ";base64," + data;
|
const url = "data:application/x-virt-viewer;base64,"
|
||||||
|
+ window.btoa(data);
|
||||||
window.open(url, target.id);
|
window.open(url, target.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<div class="jdrupes-vmoperator-vmmgmt jdrupes-vmoperator-vmmgmt-view"
|
<div class="jdrupes-vmoperator-vmmgmt jdrupes-vmoperator-vmmgmt-view"
|
||||||
data-jgwc-on-load="orgJDrupesVmOperatorVmMgmt.initView"
|
data-jgwc-on-load="orgJDrupesVmOperatorVmMgmt.initView"
|
||||||
data-jgwc-on-unload="JGConsole.jgwc.unmountVueApps">
|
data-jgwc-on-unload="JGConsole.jgwc.unmountVueApps"
|
||||||
|
data-conlet-resource-base="${conletResource('')}">
|
||||||
<div class="jdrupes-vmoperator-vmmgmt-view-search">
|
<div class="jdrupes-vmoperator-vmmgmt-view-search">
|
||||||
<form>
|
<form>
|
||||||
<label class="form__label--horizontal">
|
<label class="form__label--horizontal">
|
||||||
|
|
@ -58,17 +59,26 @@
|
||||||
</td>
|
</td>
|
||||||
<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')"
|
||||||
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')"
|
||||||
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>
|
||||||
|
<img role="button" :src="resourceBase + (!entry['running']
|
||||||
|
? 'computer-off.svg' : (entry.usedFrom
|
||||||
|
? 'computer-in-use.svg' : 'computer.svg'))"
|
||||||
|
:title="localize('Open console')"
|
||||||
|
:aria-disabled="!entry['running']
|
||||||
|
|| !(entry.permissions.includes('accessConsole'))"
|
||||||
|
v-on:click="vmAction(entry.name, 'openConsole')">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr :id="scopedId(rowIndex)" v-if="$aash.isDisclosed(scopedId(rowIndex))"
|
<tr :id="scopedId(rowIndex)" v-if="$aash.isDisclosed(scopedId(rowIndex))"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
fill="#000000"
|
||||||
|
width="800"
|
||||||
|
height="533.33331"
|
||||||
|
viewBox="0 0 24 15.999999"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="computer-in-use.svg"
|
||||||
|
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1">
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="fillet_chamfer"
|
||||||
|
id="path-effect4"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
|
||||||
|
radius="0"
|
||||||
|
unit="px"
|
||||||
|
method="auto"
|
||||||
|
mode="F"
|
||||||
|
chamfer_steps="1"
|
||||||
|
flexible="false"
|
||||||
|
use_knot_distance="true"
|
||||||
|
apply_no_radius="true"
|
||||||
|
apply_with_radius="true"
|
||||||
|
only_selected="false"
|
||||||
|
hide_knots="false" />
|
||||||
|
<linearGradient
|
||||||
|
id="swatch3"
|
||||||
|
inkscape:swatch="solid">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#000000;stop-opacity:0;"
|
||||||
|
offset="0"
|
||||||
|
id="stop3" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="0.90509668"
|
||||||
|
inkscape:cx="345.81941"
|
||||||
|
inkscape:cy="376.2029"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1008"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="35"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg1" />
|
||||||
|
<path
|
||||||
|
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"
|
||||||
|
d="m 4.7729709,13.006705 -1.7691517,0 V 0.98808897 H 20.99618 V 13.006705 l -1.639132,0"
|
||||||
|
sodipodi:nodetypes="cccccc" />
|
||||||
|
<path
|
||||||
|
id="rect2"
|
||||||
|
style="opacity:1;stroke-width:0.00145614;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:0;stroke-dasharray:none;paint-order:fill markers stroke"
|
||||||
|
d="m 0,13.998258 h 5.4336202 v 2.001741 H 0 Z"
|
||||||
|
sodipodi:nodetypes="ccccc" />
|
||||||
|
<path
|
||||||
|
id="rect3"
|
||||||
|
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.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" />
|
||||||
|
<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"
|
||||||
|
id="path3"
|
||||||
|
cx="11.964992"
|
||||||
|
cy="6.3769712"
|
||||||
|
rx="3.2413731"
|
||||||
|
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>
|
||||||
|
After Width: | Height: | Size: 3.6 KiB |
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
fill="#000000"
|
||||||
|
width="800"
|
||||||
|
height="533.33331"
|
||||||
|
viewBox="0 0 24 15.999999"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="computer-off.svg"
|
||||||
|
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1">
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="fillet_chamfer"
|
||||||
|
id="path-effect4"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
|
||||||
|
radius="0"
|
||||||
|
unit="px"
|
||||||
|
method="auto"
|
||||||
|
mode="F"
|
||||||
|
chamfer_steps="1"
|
||||||
|
flexible="false"
|
||||||
|
use_knot_distance="true"
|
||||||
|
apply_no_radius="true"
|
||||||
|
apply_with_radius="true"
|
||||||
|
only_selected="false"
|
||||||
|
hide_knots="false" />
|
||||||
|
<linearGradient
|
||||||
|
id="swatch3"
|
||||||
|
inkscape:swatch="solid">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#000000;stop-opacity:0;"
|
||||||
|
offset="0"
|
||||||
|
id="stop3" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="1.3435029"
|
||||||
|
inkscape:cx="377.74389"
|
||||||
|
inkscape:cy="227.01849"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1011"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="32"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg1" />
|
||||||
|
<path
|
||||||
|
id="rect1"
|
||||||
|
style="fill-opacity:1;stroke:#000000;stroke-width:1.97262;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:0;paint-order:fill markers stroke;fill:#545454"
|
||||||
|
d="M 3.0038192,0.98808897 H 20.99618 V 13.006705 H 3.0038192 Z" />
|
||||||
|
<rect
|
||||||
|
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"
|
||||||
|
id="rect2"
|
||||||
|
width="23.995173"
|
||||||
|
height="2.0017407"
|
||||||
|
x="0.0039473679"
|
||||||
|
y="13.998839" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
fill="#000000"
|
||||||
|
width="800"
|
||||||
|
height="533.33331"
|
||||||
|
viewBox="0 0 24 15.999999"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="computer.svg"
|
||||||
|
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1">
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="fillet_chamfer"
|
||||||
|
id="path-effect4"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
|
||||||
|
radius="0"
|
||||||
|
unit="px"
|
||||||
|
method="auto"
|
||||||
|
mode="F"
|
||||||
|
chamfer_steps="1"
|
||||||
|
flexible="false"
|
||||||
|
use_knot_distance="true"
|
||||||
|
apply_no_radius="true"
|
||||||
|
apply_with_radius="true"
|
||||||
|
only_selected="false"
|
||||||
|
hide_knots="false" />
|
||||||
|
<linearGradient
|
||||||
|
id="swatch3"
|
||||||
|
inkscape:swatch="solid">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#000000;stop-opacity:0;"
|
||||||
|
offset="0"
|
||||||
|
id="stop3" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="1.3435029"
|
||||||
|
inkscape:cx="377.74389"
|
||||||
|
inkscape:cy="227.01849"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1011"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="32"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg1" />
|
||||||
|
<path
|
||||||
|
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"
|
||||||
|
d="M 3.0038192,0.98808897 H 20.99618 V 13.006705 H 3.0038192 Z" />
|
||||||
|
<rect
|
||||||
|
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"
|
||||||
|
id="rect2"
|
||||||
|
width="23.995173"
|
||||||
|
height="2.0017407"
|
||||||
|
x="0.0039473679"
|
||||||
|
y="13.998839" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
|
|
@ -17,3 +17,8 @@ usedBy = Used by
|
||||||
usedFrom = Used from
|
usedFrom = Used from
|
||||||
vmActions = Actions
|
vmActions = Actions
|
||||||
vmname = Name
|
vmname = Name
|
||||||
|
|
||||||
|
confirmResetTitle = Confirm reset
|
||||||
|
confirmResetMsg = Resetting the VM may cause loss of data. \
|
||||||
|
Please confirm to continue.
|
||||||
|
consoleTakenNotification = Console access is locked by another user.
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,12 @@ vmname = Name
|
||||||
Value\ is\ above\ maximum = Wert ist zu groß
|
Value\ is\ above\ maximum = Wert ist zu groß
|
||||||
Illegal\ format = Ungültiges Format
|
Illegal\ format = Ungültiges Format
|
||||||
|
|
||||||
|
confirmResetTitle = Zurücksetzen bestätigen
|
||||||
|
confirmResetMsg = Zurücksetzen der VM kann zu Datenverlust führen. \
|
||||||
|
Bitte bestätigen um fortzufahren.
|
||||||
|
consoleTakenNotification = Die Konsole wird von einem anderen Benutzer verwendet.
|
||||||
|
|
||||||
|
Open\ console = Konsole anzeigen
|
||||||
Start\ VM = VM Starten
|
Start\ VM = VM Starten
|
||||||
Stop\ VM = VM Anhalten
|
Stop\ VM = VM Anhalten
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,15 +27,22 @@ import io.kubernetes.client.custom.Quantity.Format;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.net.Inet4Address;
|
||||||
|
import java.net.Inet6Address;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.jdrupes.vmoperator.common.K8sObserver;
|
import org.jdrupes.vmoperator.common.K8sObserver;
|
||||||
import org.jdrupes.vmoperator.common.VmDefinition;
|
import org.jdrupes.vmoperator.common.VmDefinition;
|
||||||
|
import org.jdrupes.vmoperator.common.VmDefinition.Permission;
|
||||||
import org.jdrupes.vmoperator.manager.events.ChannelTracker;
|
import org.jdrupes.vmoperator.manager.events.ChannelTracker;
|
||||||
|
import org.jdrupes.vmoperator.manager.events.GetDisplayPassword;
|
||||||
import org.jdrupes.vmoperator.manager.events.ModifyVm;
|
import org.jdrupes.vmoperator.manager.events.ModifyVm;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||||
|
|
@ -44,13 +51,17 @@ import org.jgrapes.core.Channel;
|
||||||
import org.jgrapes.core.Event;
|
import org.jgrapes.core.Event;
|
||||||
import org.jgrapes.core.Manager;
|
import org.jgrapes.core.Manager;
|
||||||
import org.jgrapes.core.annotation.Handler;
|
import org.jgrapes.core.annotation.Handler;
|
||||||
|
import org.jgrapes.util.events.ConfigurationUpdate;
|
||||||
import org.jgrapes.webconsole.base.Conlet.RenderMode;
|
import org.jgrapes.webconsole.base.Conlet.RenderMode;
|
||||||
import org.jgrapes.webconsole.base.ConletBaseModel;
|
import org.jgrapes.webconsole.base.ConletBaseModel;
|
||||||
import org.jgrapes.webconsole.base.ConsoleConnection;
|
import org.jgrapes.webconsole.base.ConsoleConnection;
|
||||||
import org.jgrapes.webconsole.base.events.AddConletRequest;
|
import org.jgrapes.webconsole.base.ConsoleRole;
|
||||||
|
import org.jgrapes.webconsole.base.ConsoleUser;
|
||||||
|
import org.jgrapes.webconsole.base.WebConsoleUtils;
|
||||||
import org.jgrapes.webconsole.base.events.AddConletType;
|
import org.jgrapes.webconsole.base.events.AddConletType;
|
||||||
import org.jgrapes.webconsole.base.events.AddPageResources.ScriptResource;
|
import org.jgrapes.webconsole.base.events.AddPageResources.ScriptResource;
|
||||||
import org.jgrapes.webconsole.base.events.ConsoleReady;
|
import org.jgrapes.webconsole.base.events.ConsoleReady;
|
||||||
|
import org.jgrapes.webconsole.base.events.DisplayNotification;
|
||||||
import org.jgrapes.webconsole.base.events.NotifyConletModel;
|
import org.jgrapes.webconsole.base.events.NotifyConletModel;
|
||||||
import org.jgrapes.webconsole.base.events.NotifyConletView;
|
import org.jgrapes.webconsole.base.events.NotifyConletView;
|
||||||
import org.jgrapes.webconsole.base.events.RenderConlet;
|
import org.jgrapes.webconsole.base.events.RenderConlet;
|
||||||
|
|
@ -61,10 +72,12 @@ import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet;
|
||||||
/**
|
/**
|
||||||
* The Class {@link VmMgmt}.
|
* The Class {@link VmMgmt}.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis",
|
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.CouplingBetweenObjects",
|
||||||
"PMD.CouplingBetweenObjects" })
|
"PMD.ExcessiveImports" })
|
||||||
public class VmMgmt extends FreeMarkerConlet<VmMgmt.VmsModel> {
|
public class VmMgmt extends FreeMarkerConlet<VmMgmt.VmsModel> {
|
||||||
|
|
||||||
|
private Class<?> preferredIpVersion = Inet4Address.class;
|
||||||
|
private boolean deleteConnectionFile = true;
|
||||||
private static final Set<RenderMode> MODES = RenderMode.asSet(
|
private static final Set<RenderMode> MODES = RenderMode.asSet(
|
||||||
RenderMode.Preview, RenderMode.View);
|
RenderMode.Preview, RenderMode.View);
|
||||||
private final ChannelTracker<String, VmChannel,
|
private final ChannelTracker<String, VmChannel,
|
||||||
|
|
@ -91,6 +104,44 @@ public class VmMgmt extends FreeMarkerConlet<VmMgmt.VmsModel> {
|
||||||
setPeriodicRefresh(Duration.ofMinutes(1), () -> new Update());
|
setPeriodicRefresh(Duration.ofMinutes(1), () -> new Update());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the component.
|
||||||
|
*
|
||||||
|
* @param event the event
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({ "unchecked", "PMD.AvoidDuplicateLiterals" })
|
||||||
|
@Handler
|
||||||
|
public void onConfigurationUpdate(ConfigurationUpdate event) {
|
||||||
|
event.structured("/Manager/GuiHttpServer"
|
||||||
|
+ "/ConsoleWeblet/WebConsole/ComponentCollector/VmAccess")
|
||||||
|
.ifPresent(c -> {
|
||||||
|
try {
|
||||||
|
var dispRes = (Map<String, Object>) c
|
||||||
|
.getOrDefault("displayResource",
|
||||||
|
Collections.emptyMap());
|
||||||
|
switch ((String) dispRes.getOrDefault("preferredIpVersion",
|
||||||
|
"")) {
|
||||||
|
case "ipv6":
|
||||||
|
preferredIpVersion = Inet6Address.class;
|
||||||
|
break;
|
||||||
|
case "ipv4":
|
||||||
|
default:
|
||||||
|
preferredIpVersion = Inet4Address.class;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete connection file
|
||||||
|
deleteConnectionFile
|
||||||
|
= Optional.ofNullable(c.get("deleteConnectionFile"))
|
||||||
|
.filter(v -> v instanceof String)
|
||||||
|
.map(v -> (String) v)
|
||||||
|
.map(Boolean::parseBoolean).orElse(true);
|
||||||
|
} catch (ClassCastException e) {
|
||||||
|
logger.config("Malformed configuration: " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* On {@link ConsoleReady}, fire the {@link AddConletType}.
|
* On {@link ConsoleReady}, fire the {@link AddConletType}.
|
||||||
*
|
*
|
||||||
|
|
@ -117,7 +168,7 @@ public class VmMgmt extends FreeMarkerConlet<VmMgmt.VmsModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Optional<VmsModel> createNewState(AddConletRequest event,
|
protected Optional<VmsModel> createStateRepresentation(Event<?> event,
|
||||||
ConsoleConnection connection, String conletId) throws Exception {
|
ConsoleConnection connection, String conletId) throws Exception {
|
||||||
return Optional.of(new VmsModel(conletId));
|
return Optional.of(new VmsModel(conletId));
|
||||||
}
|
}
|
||||||
|
|
@ -160,17 +211,25 @@ public class VmMgmt extends FreeMarkerConlet<VmMgmt.VmsModel> {
|
||||||
}
|
}
|
||||||
if (sendVmInfos) {
|
if (sendVmInfos) {
|
||||||
for (var item : channelTracker.values()) {
|
for (var item : channelTracker.values()) {
|
||||||
channel.respond(new NotifyConletView(type(),
|
updateVm(channel, conletId, item.associated());
|
||||||
conletId, "updateVm",
|
|
||||||
simplifiedVmDefinition(item.associated())));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderedAs;
|
return renderedAs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateVm(ConsoleConnection channel, String conletId,
|
||||||
|
VmDefinition vmDef) {
|
||||||
|
var user = WebConsoleUtils.userFromSession(channel.session())
|
||||||
|
.map(ConsoleUser::getName).orElse(null);
|
||||||
|
var roles = WebConsoleUtils.rolesFromSession(channel.session())
|
||||||
|
.stream().map(ConsoleRole::getName).toList();
|
||||||
|
channel.respond(new NotifyConletView(type(), conletId, "updateVm",
|
||||||
|
simplifiedVmDefinition(vmDef, user, roles)));
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
|
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
|
||||||
private Map<String, Object> simplifiedVmDefinition(VmDefinition vmDef) {
|
private Map<String, Object> simplifiedVmDefinition(VmDefinition vmDef,
|
||||||
|
String user, List<String> roles) {
|
||||||
// Convert RAM sizes to unitless numbers
|
// Convert RAM sizes to unitless numbers
|
||||||
var spec = DataPath.deepCopy(vmDef.spec());
|
var spec = DataPath.deepCopy(vmDef.spec());
|
||||||
var vmSpec = DataPath.<Map<String, Object>> get(spec, "vm").get();
|
var vmSpec = DataPath.<Map<String, Object>> get(spec, "vm").get();
|
||||||
|
|
@ -191,7 +250,9 @@ public class VmMgmt extends FreeMarkerConlet<VmMgmt.VmsModel> {
|
||||||
"name", vmDef.name()),
|
"name", vmDef.name()),
|
||||||
"spec", spec,
|
"spec", spec,
|
||||||
"status", status,
|
"status", status,
|
||||||
"nodeName", vmDef.extra("nodeName"));
|
"nodeName", vmDef.extra("nodeName"),
|
||||||
|
"permissions", vmDef.permissionsFor(user, roles).stream()
|
||||||
|
.map(VmDefinition.Permission::toString).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -221,8 +282,7 @@ public class VmMgmt extends FreeMarkerConlet<VmMgmt.VmsModel> {
|
||||||
channelTracker.put(vmName, channel, vmDef);
|
channelTracker.put(vmName, channel, vmDef);
|
||||||
for (var entry : conletIdsByConsoleConnection().entrySet()) {
|
for (var entry : conletIdsByConsoleConnection().entrySet()) {
|
||||||
for (String conletId : entry.getValue()) {
|
for (String conletId : entry.getValue()) {
|
||||||
entry.getKey().respond(new NotifyConletView(type(),
|
updateVm(entry.getKey(), conletId, vmDef);
|
||||||
conletId, "updateVm", simplifiedVmDefinition(vmDef)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -337,22 +397,35 @@ public class VmMgmt extends FreeMarkerConlet<VmMgmt.VmsModel> {
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings("PMD.AvoidDecimalLiteralsInBigDecimalConstructor")
|
@SuppressWarnings("PMD.AvoidDecimalLiteralsInBigDecimalConstructor")
|
||||||
protected void doUpdateConletState(NotifyConletModel event,
|
protected void doUpdateConletState(NotifyConletModel event,
|
||||||
ConsoleConnection channel, VmsModel conletState)
|
ConsoleConnection channel, VmsModel model) throws Exception {
|
||||||
throws Exception {
|
|
||||||
event.stop();
|
event.stop();
|
||||||
String vmName = event.param(0);
|
String vmName = event.param(0);
|
||||||
var vmChannel = channelTracker.channel(vmName).orElse(null);
|
var value = channelTracker.value(vmName);
|
||||||
if (vmChannel == null) {
|
var vmChannel = value.map(v -> v.channel()).orElse(null);
|
||||||
|
var vmDef = value.map(v -> v.associated()).orElse(null);
|
||||||
|
if (vmDef == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
var user = WebConsoleUtils.userFromSession(channel.session())
|
||||||
|
.map(ConsoleUser::getName).orElse("");
|
||||||
|
var roles = WebConsoleUtils.rolesFromSession(channel.session())
|
||||||
|
.stream().map(ConsoleRole::getName).toList();
|
||||||
|
var perms = vmDef.permissionsFor(user, roles);
|
||||||
switch (event.method()) {
|
switch (event.method()) {
|
||||||
case "start":
|
case "start":
|
||||||
fire(new ModifyVm(vmName, "state", "Running", vmChannel));
|
if (perms.contains(VmDefinition.Permission.START)) {
|
||||||
|
fire(new ModifyVm(vmName, "state", "Running", vmChannel));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "stop":
|
case "stop":
|
||||||
fire(new ModifyVm(vmName, "state", "Stopped", vmChannel));
|
if (perms.contains(VmDefinition.Permission.STOP)) {
|
||||||
|
fire(new ModifyVm(vmName, "state", "Stopped", vmChannel));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "openConsole":
|
case "openConsole":
|
||||||
|
if (perms.contains(VmDefinition.Permission.ACCESS_CONSOLE)) {
|
||||||
|
openConsole(channel, model, vmChannel, vmDef, user, perms);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "cpus":
|
case "cpus":
|
||||||
fire(new ModifyVm(vmName, "currentCpus",
|
fire(new ModifyVm(vmName, "currentCpus",
|
||||||
|
|
@ -370,6 +443,29 @@ public class VmMgmt extends FreeMarkerConlet<VmMgmt.VmsModel> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void openConsole(ConsoleConnection channel, VmsModel model,
|
||||||
|
VmChannel vmChannel, VmDefinition vmDef, String user,
|
||||||
|
Set<Permission> perms) {
|
||||||
|
ResourceBundle resourceBundle = resourceBundle(channel.locale());
|
||||||
|
if (!vmDef.consoleAccessible(user, perms)) {
|
||||||
|
channel.respond(new DisplayNotification(
|
||||||
|
resourceBundle.getString("consoleTakenNotification"),
|
||||||
|
Map.of("autoClose", 5_000, "type", "Warning")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var pwQuery = Event.onCompletion(new GetDisplayPassword(vmDef, user),
|
||||||
|
e -> {
|
||||||
|
var data = vmDef.connectionFile(e.password().orElse(null),
|
||||||
|
preferredIpVersion, deleteConnectionFile);
|
||||||
|
if (data == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
channel.respond(new NotifyConletView(type(),
|
||||||
|
model.getConletId(), "openConsole", data));
|
||||||
|
});
|
||||||
|
fire(pwQuery, vmChannel);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean doSetLocale(SetLocale event, ConsoleConnection channel,
|
protected boolean doSetLocale(SetLocale event, ConsoleConnection channel,
|
||||||
String conletId) throws Exception {
|
String conletId) throws Exception {
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,7 @@ window.orgJDrupesVmOperatorVmMgmt.initView = (viewDom: HTMLElement,
|
||||||
setup(_props: object) {
|
setup(_props: object) {
|
||||||
const conletId: string
|
const conletId: string
|
||||||
= (<HTMLElement>viewDom.parentNode!).dataset["conletId"]!;
|
= (<HTMLElement>viewDom.parentNode!).dataset["conletId"]!;
|
||||||
|
const resourceBase = (<HTMLElement>viewDom).dataset.conletResourceBase;
|
||||||
|
|
||||||
const controller = reactive(new JGConsole.TableController([
|
const controller = reactive(new JGConsole.TableController([
|
||||||
["name", "vmname"],
|
["name", "vmname"],
|
||||||
|
|
@ -162,9 +163,9 @@ window.orgJDrupesVmOperatorVmMgmt.initView = (viewDom: HTMLElement,
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
controller, vmInfos, filteredData, detailsByName, localize,
|
controller, vmInfos, filteredData, detailsByName,
|
||||||
shortDateTime, formatMemory, vmAction, cic, parseMemory,
|
resourceBase, localize, shortDateTime, formatMemory,
|
||||||
maximumCpus,
|
vmAction, cic, parseMemory, maximumCpus,
|
||||||
scopedId: (id: string) => { return idScope.scopedId(id); }
|
scopedId: (id: string) => { return idScope.scopedId(id); }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -219,3 +220,20 @@ JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmmgmt.VmMgmt",
|
||||||
Object.assign(vmSummary, summary);
|
Object.assign(vmSummary, summary);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmmgmt.VmMgmt",
|
||||||
|
"openConsole", function(_conletId: string, data: string) {
|
||||||
|
let target = document.getElementById(
|
||||||
|
"org.jdrupes.vmoperator.vmmgt.VmMgmt.target");
|
||||||
|
if (!target) {
|
||||||
|
target = document.createElement("iframe");
|
||||||
|
target.id = "org.jdrupes.vmoperator.vmmgt.VmMgmt.target";
|
||||||
|
target.setAttribute("name", target.id);
|
||||||
|
target.setAttribute("style", "display: none;");
|
||||||
|
document.querySelector("body")!.append(target);
|
||||||
|
}
|
||||||
|
const url = "data:application/x-virt-viewer;base64,"
|
||||||
|
+ window.btoa(data);
|
||||||
|
window.open(url, target.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,10 @@
|
||||||
.jdrupes-vmoperator-vmmgmt-view-action-list {
|
.jdrupes-vmoperator-vmmgmt-view-action-list {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
|
& > * + * {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
[role=button] {
|
[role=button] {
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
|
|
||||||
|
|
@ -110,4 +114,14 @@
|
||||||
box-shadow: var(--darkening);
|
box-shadow: var(--darkening);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: inline;
|
||||||
|
height: 1.5em;
|
||||||
|
vertical-align: top;
|
||||||
|
|
||||||
|
&[aria-disabled=''], &[aria-disabled='true'] {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue