Add console access to VM management.

This commit is contained in:
Michael Lipp 2025-01-29 17:33:16 +01:00
parent 5cd4edcec1
commit af41c78c07
14 changed files with 561 additions and 124 deletions

View file

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

View file

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