Delay console opening for pool VMs.
This commit is contained in:
parent
c582763fbf
commit
5ad052ffe4
6 changed files with 191 additions and 119 deletions
|
|
@ -1,74 +0,0 @@
|
|||
/*
|
||||
* VM-Operator
|
||||
* Copyright (C) 2024 Michael N. Lipp
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.jdrupes.vmoperator.manager.events;
|
||||
|
||||
import java.util.Optional;
|
||||
import org.jdrupes.vmoperator.common.VmDefinition;
|
||||
import org.jgrapes.core.Event;
|
||||
|
||||
/**
|
||||
* Gets the current display secret and optionally updates it.
|
||||
*/
|
||||
@SuppressWarnings("PMD.DataClass")
|
||||
public class GetDisplayPassword extends Event<String> {
|
||||
|
||||
private final VmDefinition vmDef;
|
||||
private final String user;
|
||||
|
||||
/**
|
||||
* Instantiates a new request for the display secret.
|
||||
*
|
||||
* @param vmDef the vm name
|
||||
* @param user the requesting user
|
||||
*/
|
||||
public GetDisplayPassword(VmDefinition vmDef, String user) {
|
||||
this.vmDef = vmDef;
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the vm definition.
|
||||
*
|
||||
* @return the vm definition
|
||||
*/
|
||||
public VmDefinition vmDefinition() {
|
||||
return vmDef;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the id of the user who has requested the password.
|
||||
*
|
||||
* @return the string
|
||||
*/
|
||||
public String user() {
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the password. May only be called when the event is completed.
|
||||
*
|
||||
* @return the optional
|
||||
*/
|
||||
public Optional<String> password() {
|
||||
if (!isDone()) {
|
||||
throw new IllegalStateException("Event is not done.");
|
||||
}
|
||||
return currentResults().stream().findFirst();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* VM-Operator
|
||||
* Copyright (C) 2024 Michael N. Lipp
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.jdrupes.vmoperator.manager.events;
|
||||
|
||||
import org.jdrupes.vmoperator.common.VmDefinition;
|
||||
import org.jgrapes.core.Event;
|
||||
|
||||
/**
|
||||
* Gets the current display secret and optionally updates it.
|
||||
*/
|
||||
@SuppressWarnings("PMD.DataClass")
|
||||
public class PrepareConsole extends Event<String> {
|
||||
|
||||
private final VmDefinition vmDef;
|
||||
private final String user;
|
||||
private final boolean loginUser;
|
||||
|
||||
/**
|
||||
* Instantiates a new request for the display secret.
|
||||
* After handling the event, a result of `null` means that
|
||||
* no password is needed. No result means that the console
|
||||
* is not accessible.
|
||||
*
|
||||
* @param vmDef the vm name
|
||||
* @param user the requesting user
|
||||
* @param loginUser login the user
|
||||
*/
|
||||
public PrepareConsole(VmDefinition vmDef, String user,
|
||||
boolean loginUser) {
|
||||
this.vmDef = vmDef;
|
||||
this.user = user;
|
||||
this.loginUser = loginUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new request for the display secret.
|
||||
* After handling the event, a result of `null` means that
|
||||
* no password is needed. No result means that the console
|
||||
* is not accessible.
|
||||
*
|
||||
* @param vmDef the vm name
|
||||
* @param user the requesting user
|
||||
*/
|
||||
public PrepareConsole(VmDefinition vmDef, String user) {
|
||||
this(vmDef, user, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the vm definition.
|
||||
*
|
||||
* @return the vm definition
|
||||
*/
|
||||
public VmDefinition vmDefinition() {
|
||||
return vmDef;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the id of the user who has requested the password.
|
||||
*
|
||||
* @return the string
|
||||
*/
|
||||
public String user() {
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user should be logged in before allowing access.
|
||||
*
|
||||
* @return the loginUser
|
||||
*/
|
||||
public boolean loginUser() {
|
||||
return loginUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if a password is available. May only be called
|
||||
* when the event is completed. Note that the password returned
|
||||
* by {@link #password()} may be `null`, indicating that no password
|
||||
* is needed.
|
||||
*
|
||||
* @return true, if successful
|
||||
*/
|
||||
public boolean passwordAvailable() {
|
||||
if (!isDone()) {
|
||||
throw new IllegalStateException("Event is not done.");
|
||||
}
|
||||
return !currentResults().isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the password. May only be called when the event has been
|
||||
* completed with a valid result (see {@link #passwordAvailable()}).
|
||||
*
|
||||
* @return the password. A value of `null` means that no password
|
||||
* is required.
|
||||
*/
|
||||
public String password() {
|
||||
if (!isDone() || currentResults().isEmpty()) {
|
||||
throw new IllegalStateException("Event is not done.");
|
||||
}
|
||||
return currentResults().get(0);
|
||||
}
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET;
|
|||
import static org.jdrupes.vmoperator.manager.Constants.DATA_DISPLAY_PASSWORD;
|
||||
import static org.jdrupes.vmoperator.manager.Constants.DATA_PASSWORD_EXPIRY;
|
||||
import org.jdrupes.vmoperator.manager.events.ChannelDictionary;
|
||||
import org.jdrupes.vmoperator.manager.events.GetDisplayPassword;
|
||||
import org.jdrupes.vmoperator.manager.events.PrepareConsole;
|
||||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||
import org.jgrapes.core.Channel;
|
||||
|
|
@ -72,7 +72,7 @@ public class DisplaySecretMonitor
|
|||
extends AbstractMonitor<V1Secret, V1SecretList, VmChannel> {
|
||||
|
||||
private int passwordValidity = 10;
|
||||
private final List<PendingGet> pendingGets
|
||||
private final List<PendingGet> pendingPrepares
|
||||
= Collections.synchronizedList(new LinkedList<>());
|
||||
private final ChannelDictionary<String, VmChannel, ?> channelDictionary;
|
||||
|
||||
|
|
@ -178,49 +178,59 @@ public class DisplaySecretMonitor
|
|||
*/
|
||||
@Handler
|
||||
@SuppressWarnings("PMD.StringInstantiation")
|
||||
public void onGetDisplaySecrets(GetDisplayPassword event, VmChannel channel)
|
||||
public void onPrepareConsole(PrepareConsole event, VmChannel channel)
|
||||
throws ApiException {
|
||||
// Update console user in status
|
||||
var vmStub = VmDefinitionStub.get(client(),
|
||||
new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM),
|
||||
event.vmDefinition().namespace(), event.vmDefinition().name());
|
||||
vmStub.updateStatus(from -> {
|
||||
var optVmDef = vmStub.updateStatus(from -> {
|
||||
JsonObject status = from.statusJson();
|
||||
status.addProperty("consoleUser", event.user());
|
||||
return status;
|
||||
});
|
||||
if (optVmDef.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
var vmDef = optVmDef.get();
|
||||
|
||||
// Check if access is possible
|
||||
if (event.loginUser()
|
||||
? !vmDef.conditionStatus("Booted").orElse(false)
|
||||
: !vmDef.conditionStatus("Running").orElse(false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Look for secret
|
||||
ListOptions options = new ListOptions();
|
||||
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
|
||||
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET + ","
|
||||
+ "app.kubernetes.io/instance="
|
||||
+ event.vmDefinition().metadata().getName());
|
||||
var stubs = K8sV1SecretStub.list(client(),
|
||||
event.vmDefinition().namespace(), options);
|
||||
+ "app.kubernetes.io/instance=" + vmDef.name());
|
||||
var stubs = K8sV1SecretStub.list(client(), vmDef.namespace(), options);
|
||||
if (stubs.isEmpty()) {
|
||||
// No secret means no password for this VM wanted
|
||||
event.setResult(null);
|
||||
return;
|
||||
}
|
||||
var stub = stubs.iterator().next();
|
||||
|
||||
// Check validity
|
||||
var model = stub.model().get();
|
||||
var secret = stub.model().get();
|
||||
@SuppressWarnings("PMD.StringInstantiation")
|
||||
var expiry = Optional.ofNullable(model.getData()
|
||||
var expiry = Optional.ofNullable(secret.getData()
|
||||
.get(DATA_PASSWORD_EXPIRY)).map(b -> new String(b)).orElse(null);
|
||||
if (model.getData().get(DATA_DISPLAY_PASSWORD) != null
|
||||
if (secret.getData().get(DATA_DISPLAY_PASSWORD) != null
|
||||
&& stillValid(expiry)) {
|
||||
// Fixed secret, don't touch
|
||||
event.setResult(
|
||||
new String(model.getData().get(DATA_DISPLAY_PASSWORD)));
|
||||
new String(secret.getData().get(DATA_DISPLAY_PASSWORD)));
|
||||
return;
|
||||
}
|
||||
updatePassword(stub, event);
|
||||
}
|
||||
|
||||
@SuppressWarnings("PMD.StringInstantiation")
|
||||
private void updatePassword(K8sV1SecretStub stub, GetDisplayPassword event)
|
||||
private void updatePassword(K8sV1SecretStub stub, PrepareConsole event)
|
||||
throws ApiException {
|
||||
SecureRandom random = null;
|
||||
try {
|
||||
|
|
@ -242,9 +252,9 @@ public class DisplaySecretMonitor
|
|||
var pending = new PendingGet(event,
|
||||
event.vmDefinition().displayPasswordSerial().orElse(0L) + 1,
|
||||
new CompletionLock(event, 1500));
|
||||
pendingGets.add(pending);
|
||||
pendingPrepares.add(pending);
|
||||
Event.onCompletion(event, e -> {
|
||||
pendingGets.remove(pending);
|
||||
pendingPrepares.remove(pending);
|
||||
});
|
||||
|
||||
// Update, will (eventually) trigger confirmation
|
||||
|
|
@ -273,9 +283,9 @@ public class DisplaySecretMonitor
|
|||
@Handler
|
||||
@SuppressWarnings("PMD.AvoidSynchronizedStatement")
|
||||
public void onVmDefChanged(VmDefChanged event, Channel channel) {
|
||||
synchronized (pendingGets) {
|
||||
synchronized (pendingPrepares) {
|
||||
String vmName = event.vmDefinition().name();
|
||||
for (var pending : pendingGets) {
|
||||
for (var pending : pendingPrepares) {
|
||||
if (pending.event.vmDefinition().name().equals(vmName)
|
||||
&& event.vmDefinition().displayPasswordSerial()
|
||||
.map(s -> s >= pending.expectedSerial).orElse(false)) {
|
||||
|
|
@ -293,7 +303,7 @@ public class DisplaySecretMonitor
|
|||
*/
|
||||
@SuppressWarnings("PMD.DataClass")
|
||||
private static class PendingGet {
|
||||
public final GetDisplayPassword event;
|
||||
public final PrepareConsole event;
|
||||
public final long expectedSerial;
|
||||
public final CompletionLock lock;
|
||||
|
||||
|
|
@ -303,7 +313,7 @@ public class DisplaySecretMonitor
|
|||
* @param event the event
|
||||
* @param expectedSerial the expected serial
|
||||
*/
|
||||
public PendingGet(GetDisplayPassword event, long expectedSerial,
|
||||
public PendingGet(PrepareConsole event, long expectedSerial,
|
||||
CompletionLock lock) {
|
||||
super();
|
||||
this.event = event;
|
||||
|
|
|
|||
|
|
@ -49,11 +49,11 @@ import org.jdrupes.vmoperator.common.VmDefinition;
|
|||
import org.jdrupes.vmoperator.common.VmDefinition.Permission;
|
||||
import org.jdrupes.vmoperator.common.VmPool;
|
||||
import org.jdrupes.vmoperator.manager.events.AssignVm;
|
||||
import org.jdrupes.vmoperator.manager.events.GetDisplayPassword;
|
||||
import org.jdrupes.vmoperator.manager.events.GetPools;
|
||||
import org.jdrupes.vmoperator.manager.events.GetVms;
|
||||
import org.jdrupes.vmoperator.manager.events.GetVms.VmData;
|
||||
import org.jdrupes.vmoperator.manager.events.ModifyVm;
|
||||
import org.jdrupes.vmoperator.manager.events.PrepareConsole;
|
||||
import org.jdrupes.vmoperator.manager.events.ResetVm;
|
||||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||
|
|
@ -808,18 +808,23 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
|
|||
Map.of("autoClose", 5_000, "type", "Warning")));
|
||||
return;
|
||||
}
|
||||
var pwQuery = Event.onCompletion(new GetDisplayPassword(vmDef, user),
|
||||
e -> {
|
||||
vmDef.extra()
|
||||
.map(xtra -> xtra.connectionFile(e.password().orElse(null),
|
||||
preferredIpVersion, deleteConnectionFile))
|
||||
.ifPresent(
|
||||
cf -> channel.respond(new NotifyConletView(type(),
|
||||
model.getConletId(), "openConsole", cf)));
|
||||
});
|
||||
var pwQuery = Event.onCompletion(new PrepareConsole(vmDef, user,
|
||||
model.mode() == ResourceModel.Mode.POOL),
|
||||
e -> gotPassword(channel, model, vmDef, e));
|
||||
fire(pwQuery, vmChannel);
|
||||
}
|
||||
|
||||
private void gotPassword(ConsoleConnection channel, ResourceModel model,
|
||||
VmDefinition vmDef, PrepareConsole event) {
|
||||
if (!event.passwordAvailable()) {
|
||||
return;
|
||||
}
|
||||
vmDef.extra().map(xtra -> xtra.connectionFile(event.password(),
|
||||
preferredIpVersion, deleteConnectionFile))
|
||||
.ifPresent(cf -> channel.respond(new NotifyConletView(type(),
|
||||
model.getConletId(), "openConsole", cf)));
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition",
|
||||
"PMD.UseLocaleWithCaseConversions" })
|
||||
private void selectResource(NotifyConletModel event,
|
||||
|
|
|
|||
|
|
@ -73,7 +73,9 @@ window.orgJDrupesVmOperatorVmAccess.initPreview = (previewDom: HTMLElement,
|
|||
const configured = computed(() => previewApi.vmDefinition.spec);
|
||||
const busy = computed(() => previewApi.vmDefinition.spec
|
||||
&& (previewApi.vmDefinition.spec.vm.state === 'Running'
|
||||
&& !previewApi.vmDefinition.running
|
||||
&& (previewApi.poolName
|
||||
? !previewApi.vmDefinition.booted
|
||||
: !previewApi.vmDefinition.running)
|
||||
|| previewApi.vmDefinition.spec.vm.state === 'Stopped'
|
||||
&& previewApi.vmDefinition.running));
|
||||
const startable = computed(() => previewApi.vmDefinition.spec
|
||||
|
|
@ -85,6 +87,7 @@ window.orgJDrupesVmOperatorVmAccess.initPreview = (previewDom: HTMLElement,
|
|||
previewApi.vmDefinition.spec.vm.state !== 'Stopped'
|
||||
&& previewApi.vmDefinition.running);
|
||||
const running = computed(() => previewApi.vmDefinition.running);
|
||||
const booted = computed(() => previewApi.vmDefinition.booted);
|
||||
const inUse = computed(() => previewApi.vmDefinition.usedBy != '');
|
||||
const permissions = computed(() => previewApi.permissions);
|
||||
const osicon = computed(() => {
|
||||
|
|
@ -120,8 +123,8 @@ window.orgJDrupesVmOperatorVmAccess.initPreview = (previewDom: HTMLElement,
|
|||
};
|
||||
|
||||
return { localize, resourceBase, vmAction, poolName, vmName,
|
||||
configured, busy, startable, stoppable, running, inUse,
|
||||
permissions, osicon };
|
||||
configured, busy, startable, stoppable, running, booted,
|
||||
inUse, permissions, osicon };
|
||||
},
|
||||
template: `
|
||||
<table>
|
||||
|
|
@ -129,7 +132,8 @@ window.orgJDrupesVmOperatorVmAccess.initPreview = (previewDom: HTMLElement,
|
|||
<tr>
|
||||
<td rowspan="2" style="position: relative"><span
|
||||
style="position: absolute;" :class="{ busy: busy }"
|
||||
><img role=button :aria-disabled="!running
|
||||
><img role=button :aria-disabled="(poolName
|
||||
? !booted : !running)
|
||||
|| !permissions.includes('accessConsole')"
|
||||
v-on:click="vmAction('openConsole')"
|
||||
:src="resourceBase + (running
|
||||
|
|
@ -206,14 +210,17 @@ JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmaccess.VmAccess",
|
|||
vmDefinition.currentCpus = vmDefinition.status.cpus;
|
||||
vmDefinition.currentRam = Number(vmDefinition.status.ram);
|
||||
vmDefinition.usedBy = vmDefinition.status.consoleClient || "";
|
||||
for (const condition of vmDefinition.status.conditions) {
|
||||
vmDefinition.status.conditions.forEach((condition: any) => {
|
||||
if (condition.type === "Running") {
|
||||
vmDefinition.running = condition.status === "True";
|
||||
vmDefinition.runningConditionSince
|
||||
= new Date(condition.lastTransitionTime);
|
||||
break;
|
||||
} else if (condition.type === "Booted") {
|
||||
vmDefinition.booted = condition.status === "True";
|
||||
vmDefinition.bootedConditionSince
|
||||
= new Date(condition.lastTransitionTime);
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
vmDefinition = {};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ import org.jdrupes.vmoperator.common.VmDefinition;
|
|||
import org.jdrupes.vmoperator.common.VmDefinition.Permission;
|
||||
import org.jdrupes.vmoperator.common.VmExtraData;
|
||||
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.PrepareConsole;
|
||||
import org.jdrupes.vmoperator.manager.events.ResetVm;
|
||||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||
|
|
@ -483,17 +483,22 @@ public class VmMgmt extends FreeMarkerConlet<VmMgmt.VmsModel> {
|
|||
Map.of("autoClose", 5_000, "type", "Warning")));
|
||||
return;
|
||||
}
|
||||
var pwQuery = Event.onCompletion(new GetDisplayPassword(vmDef, user),
|
||||
e -> {
|
||||
vmDef.extra().map(xtra -> xtra.connectionFile(
|
||||
e.password().orElse(null), preferredIpVersion,
|
||||
deleteConnectionFile)).ifPresent(
|
||||
cf -> channel.respond(new NotifyConletView(type(),
|
||||
model.getConletId(), "openConsole", cf)));
|
||||
});
|
||||
var pwQuery = Event.onCompletion(new PrepareConsole(vmDef, user),
|
||||
e -> gotPassword(channel, model, vmDef, e));
|
||||
fire(pwQuery, vmChannel);
|
||||
}
|
||||
|
||||
private void gotPassword(ConsoleConnection channel, VmsModel model,
|
||||
VmDefinition vmDef, PrepareConsole event) {
|
||||
if (!event.passwordAvailable()) {
|
||||
return;
|
||||
}
|
||||
vmDef.extra().map(xtra -> xtra.connectionFile(event.password(),
|
||||
preferredIpVersion, deleteConnectionFile)).ifPresent(
|
||||
cf -> channel.respond(new NotifyConletView(type(),
|
||||
model.getConletId(), "openConsole", cf)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doSetLocale(SetLocale event, ConsoleConnection channel,
|
||||
String conletId) throws Exception {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue