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:
|
||||
- "*"
|
||||
operator:
|
||||
- org.jdrupes.vmoperator.vmmgmt.VmMgmt
|
||||
- org.jdrupes.vmoperator.vmaccess.VmAccess
|
||||
user:
|
||||
- org.jdrupes.vmoperator.vmaccess.VmAccess
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* 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
|
||||
* 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 io.kubernetes.client.openapi.models.V1Condition;
|
||||
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.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
|
@ -34,6 +38,8 @@ import java.util.Optional;
|
|||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.stream.Collectors;
|
||||
import org.jdrupes.vmoperator.util.DataPath;
|
||||
|
||||
|
|
@ -43,7 +49,11 @@ import org.jdrupes.vmoperator.util.DataPath;
|
|||
@SuppressWarnings({ "PMD.DataClass", "PMD.TooManyMethods" })
|
||||
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());
|
||||
|
||||
private String kind;
|
||||
|
|
@ -427,6 +437,8 @@ public class VmDefinition {
|
|||
|
||||
/**
|
||||
* 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 roles the roles
|
||||
|
|
@ -434,7 +446,7 @@ public class VmDefinition {
|
|||
*/
|
||||
public Set<Permission> permissionsFor(String user,
|
||||
Collection<String> roles) {
|
||||
return this.<List<Map<String, Object>>> fromSpec("permissions")
|
||||
var result = this.<List<Map<String, Object>>> fromSpec("permissions")
|
||||
.orElse(Collections.emptyList()).stream()
|
||||
.filter(p -> DataPath.get(p, "user").map(u -> u.equals(user))
|
||||
.orElse(false)
|
||||
|
|
@ -443,7 +455,29 @@ public class VmDefinition {
|
|||
.orElse(Collections.emptyList()).stream())
|
||||
.flatMap(Function.identity())
|
||||
.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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import java.util.ArrayList;
|
|||
import java.util.Comparator;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
|
||||
|
|
@ -125,7 +126,12 @@ public class VmMonitor extends
|
|||
protected void handleChange(K8sClient client,
|
||||
Watch.Response<VmDefinitionModel> response) {
|
||||
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
|
||||
var vmModel = response.object;
|
||||
|
|
@ -151,6 +157,9 @@ public class VmMonitor extends
|
|||
+ response.object.getMetadata());
|
||||
return;
|
||||
}
|
||||
if (toBeAdded.get()) {
|
||||
channelManager.put(vmDef.name(), channel);
|
||||
}
|
||||
|
||||
// Create and fire changed event. Remove channel from channel
|
||||
// manager on completion.
|
||||
|
|
|
|||
|
|
@ -32,10 +32,7 @@ import io.kubernetes.client.util.Strings;
|
|||
import java.io.IOException;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.time.Duration;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
|
|
@ -779,24 +776,8 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
|
|||
}
|
||||
break;
|
||||
case "openConsole":
|
||||
var user = WebConsoleUtils.userFromSession(channel.session())
|
||||
.map(ConsoleUser::getName).orElse("");
|
||||
if (vmDef.conditionStatus("ConsoleConnected").orElse(false)
|
||||
&& vmDef.consoleUser().map(cu -> !cu.equals(user)
|
||||
&& !perms.contains(VmDefinition.Permission.TAKE_CONSOLE))
|
||||
.orElse(false)) {
|
||||
channel.respond(new DisplayNotification(
|
||||
resourceBundle.getString("consoleTakenNotification"),
|
||||
Map.of("autoClose", 5_000, "type", "Warning")));
|
||||
return;
|
||||
}
|
||||
if (perms.contains(VmDefinition.Permission.ACCESS_CONSOLE)
|
||||
|| perms.contains(VmDefinition.Permission.TAKE_CONSOLE)) {
|
||||
var pwQuery
|
||||
= Event.onCompletion(new GetDisplayPassword(vmDef, user),
|
||||
e -> openConsole(vmDef, channel, model,
|
||||
e.password().orElse(null)));
|
||||
fire(pwQuery, vmChannel);
|
||||
if (perms.contains(VmDefinition.Permission.ACCESS_CONSOLE)) {
|
||||
openConsole(channel, model, vmChannel, vmDef, perms);
|
||||
}
|
||||
break;
|
||||
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",
|
||||
"PMD.UseLocaleWithCaseConversions" })
|
||||
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,
|
||||
ConsoleConnection channel, ResourceModel model,
|
||||
ResourceBundle resourceBundle) throws TemplateNotFoundException,
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ 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(
|
||||
"org.jdrupes.vmoperator.vmaccess.VmAccess.target");
|
||||
if (!target) {
|
||||
|
|
@ -208,7 +208,8 @@ JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmaccess.VmAccess",
|
|||
target.setAttribute("style", "display: none;");
|
||||
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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<div class="jdrupes-vmoperator-vmmgmt jdrupes-vmoperator-vmmgmt-view"
|
||||
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">
|
||||
<form>
|
||||
<label class="form__label--horizontal">
|
||||
|
|
@ -58,17 +59,26 @@
|
|||
</td>
|
||||
<td class="jdrupes-vmoperator-vmmgmt-view-action-list">
|
||||
<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')"
|
||||
v-on:click="vmAction(entry.name, 'start')"></span>
|
||||
<span role="button" v-else class="fa fa-play"
|
||||
aria-disabled="true" :title="localize('Start VM')"></span>
|
||||
<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')"
|
||||
v-on:click="vmAction(entry.name, 'stop')"></span>
|
||||
<span role="button" v-else class="fa fa-stop"
|
||||
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>
|
||||
</tr>
|
||||
<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
|
||||
vmActions = Actions
|
||||
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ß
|
||||
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
|
||||
Stop\ VM = VM Anhalten
|
||||
|
||||
|
|
|
|||
|
|
@ -27,15 +27,22 @@ import io.kubernetes.client.custom.Quantity.Format;
|
|||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.Set;
|
||||
import org.jdrupes.vmoperator.common.K8sObserver;
|
||||
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.GetDisplayPassword;
|
||||
import org.jdrupes.vmoperator.manager.events.ModifyVm;
|
||||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||
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.Manager;
|
||||
import org.jgrapes.core.annotation.Handler;
|
||||
import org.jgrapes.util.events.ConfigurationUpdate;
|
||||
import org.jgrapes.webconsole.base.Conlet.RenderMode;
|
||||
import org.jgrapes.webconsole.base.ConletBaseModel;
|
||||
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.AddPageResources.ScriptResource;
|
||||
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.NotifyConletView;
|
||||
import org.jgrapes.webconsole.base.events.RenderConlet;
|
||||
|
|
@ -61,10 +72,12 @@ import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet;
|
|||
/**
|
||||
* The Class {@link VmMgmt}.
|
||||
*/
|
||||
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis",
|
||||
"PMD.CouplingBetweenObjects" })
|
||||
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.CouplingBetweenObjects",
|
||||
"PMD.ExcessiveImports" })
|
||||
public class VmMgmt extends FreeMarkerConlet<VmMgmt.VmsModel> {
|
||||
|
||||
private Class<?> preferredIpVersion = Inet4Address.class;
|
||||
private boolean deleteConnectionFile = true;
|
||||
private static final Set<RenderMode> MODES = RenderMode.asSet(
|
||||
RenderMode.Preview, RenderMode.View);
|
||||
private final ChannelTracker<String, VmChannel,
|
||||
|
|
@ -91,6 +104,44 @@ public class VmMgmt extends FreeMarkerConlet<VmMgmt.VmsModel> {
|
|||
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}.
|
||||
*
|
||||
|
|
@ -117,7 +168,7 @@ public class VmMgmt extends FreeMarkerConlet<VmMgmt.VmsModel> {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected Optional<VmsModel> createNewState(AddConletRequest event,
|
||||
protected Optional<VmsModel> createStateRepresentation(Event<?> event,
|
||||
ConsoleConnection connection, String conletId) throws Exception {
|
||||
return Optional.of(new VmsModel(conletId));
|
||||
}
|
||||
|
|
@ -160,17 +211,25 @@ public class VmMgmt extends FreeMarkerConlet<VmMgmt.VmsModel> {
|
|||
}
|
||||
if (sendVmInfos) {
|
||||
for (var item : channelTracker.values()) {
|
||||
channel.respond(new NotifyConletView(type(),
|
||||
conletId, "updateVm",
|
||||
simplifiedVmDefinition(item.associated())));
|
||||
updateVm(channel, conletId, item.associated());
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
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
|
||||
var spec = DataPath.deepCopy(vmDef.spec());
|
||||
var vmSpec = DataPath.<Map<String, Object>> get(spec, "vm").get();
|
||||
|
|
@ -191,7 +250,9 @@ public class VmMgmt extends FreeMarkerConlet<VmMgmt.VmsModel> {
|
|||
"name", vmDef.name()),
|
||||
"spec", spec,
|
||||
"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);
|
||||
for (var entry : conletIdsByConsoleConnection().entrySet()) {
|
||||
for (String conletId : entry.getValue()) {
|
||||
entry.getKey().respond(new NotifyConletView(type(),
|
||||
conletId, "updateVm", simplifiedVmDefinition(vmDef)));
|
||||
updateVm(entry.getKey(), conletId, vmDef);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -337,22 +397,35 @@ public class VmMgmt extends FreeMarkerConlet<VmMgmt.VmsModel> {
|
|||
@Override
|
||||
@SuppressWarnings("PMD.AvoidDecimalLiteralsInBigDecimalConstructor")
|
||||
protected void doUpdateConletState(NotifyConletModel event,
|
||||
ConsoleConnection channel, VmsModel conletState)
|
||||
throws Exception {
|
||||
ConsoleConnection channel, VmsModel model) throws Exception {
|
||||
event.stop();
|
||||
String vmName = event.param(0);
|
||||
var vmChannel = channelTracker.channel(vmName).orElse(null);
|
||||
if (vmChannel == null) {
|
||||
var value = channelTracker.value(vmName);
|
||||
var vmChannel = value.map(v -> v.channel()).orElse(null);
|
||||
var vmDef = value.map(v -> v.associated()).orElse(null);
|
||||
if (vmDef == null) {
|
||||
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()) {
|
||||
case "start":
|
||||
fire(new ModifyVm(vmName, "state", "Running", vmChannel));
|
||||
if (perms.contains(VmDefinition.Permission.START)) {
|
||||
fire(new ModifyVm(vmName, "state", "Running", vmChannel));
|
||||
}
|
||||
break;
|
||||
case "stop":
|
||||
fire(new ModifyVm(vmName, "state", "Stopped", vmChannel));
|
||||
if (perms.contains(VmDefinition.Permission.STOP)) {
|
||||
fire(new ModifyVm(vmName, "state", "Stopped", vmChannel));
|
||||
}
|
||||
break;
|
||||
case "openConsole":
|
||||
if (perms.contains(VmDefinition.Permission.ACCESS_CONSOLE)) {
|
||||
openConsole(channel, model, vmChannel, vmDef, user, perms);
|
||||
}
|
||||
break;
|
||||
case "cpus":
|
||||
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
|
||||
protected boolean doSetLocale(SetLocale event, ConsoleConnection channel,
|
||||
String conletId) throws Exception {
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ window.orgJDrupesVmOperatorVmMgmt.initView = (viewDom: HTMLElement,
|
|||
setup(_props: object) {
|
||||
const conletId: string
|
||||
= (<HTMLElement>viewDom.parentNode!).dataset["conletId"]!;
|
||||
const resourceBase = (<HTMLElement>viewDom).dataset.conletResourceBase;
|
||||
|
||||
const controller = reactive(new JGConsole.TableController([
|
||||
["name", "vmname"],
|
||||
|
|
@ -162,9 +163,9 @@ window.orgJDrupesVmOperatorVmMgmt.initView = (viewDom: HTMLElement,
|
|||
}
|
||||
|
||||
return {
|
||||
controller, vmInfos, filteredData, detailsByName, localize,
|
||||
shortDateTime, formatMemory, vmAction, cic, parseMemory,
|
||||
maximumCpus,
|
||||
controller, vmInfos, filteredData, detailsByName,
|
||||
resourceBase, localize, shortDateTime, formatMemory,
|
||||
vmAction, cic, parseMemory, maximumCpus,
|
||||
scopedId: (id: string) => { return idScope.scopedId(id); }
|
||||
};
|
||||
}
|
||||
|
|
@ -219,3 +220,20 @@ JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmmgmt.VmMgmt",
|
|||
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 {
|
||||
white-space: nowrap;
|
||||
|
||||
& > * + * {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
padding: 0.25rem;
|
||||
|
||||
|
|
@ -110,4 +114,14 @@
|
|||
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