diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModel.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModel.java index 6a4410f..dd2bdd5 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModel.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModel.java @@ -102,7 +102,7 @@ public class K8sDynamicModel implements KubernetesObject { * * @return the JSON object describing the status */ - public JsonObject status() { + public JsonObject statusJson() { return data.getAsJsonObject("status"); } diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java index 0689a97..688f43f 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java @@ -193,42 +193,41 @@ public class K8sGenericStub updateStatus(O object, - Function status, int retries) throws ApiException { - while (true) { - try { - return K8s.optional(api.updateStatus(object, status)); - } catch (ApiException e) { - if (HttpURLConnection.HTTP_CONFLICT != e.getCode() - || retries-- <= 0) { - throw e; - } - } - } + public Optional updateStatus(O object, Function status) + throws ApiException { + return K8s.optional(api.updateStatus(object, status)); } /** - * Updates the object's status, retrying up to 16 times if there - * is a conflict. + * Gets the object and updates the status. In case of conflict, retries + * up to `retries` times. * - * @param object the current state of the object (passed to `status`) - * @param status function that returns the new status - * @return the updated model or empty if not successful + * @param status the status + * @param retries the retries in case of conflict + * @return the updated model or empty if the object was not found * @throws ApiException the api exception */ - public Optional updateStatus(O object, - Function status) throws ApiException { - return updateStatus(object, status, 16); + @SuppressWarnings({ "PMD.AssignmentInOperand", "PMD.UnusedAssignment" }) + public Optional updateStatus(Function status, int retries) + throws ApiException { + try { + return updateStatus(api.get(namespace, name).throwsApiException() + .getObject(), status); + } catch (ApiException e) { + if (HttpURLConnection.HTTP_CONFLICT != e.getCode() + || retries-- <= 0) { + throw e; + } + } + return Optional.empty(); } /** @@ -241,8 +240,7 @@ public class K8sGenericStub updateStatus(Function status) throws ApiException { - return updateStatus( - api.get(namespace, name).throwsApiException().getObject(), status); + return updateStatus(status, 16); } /** diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinition.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinition.java index ffb1bf2..ec79b80 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinition.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinition.java @@ -20,11 +20,10 @@ package org.jdrupes.vmoperator.common; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import io.kubernetes.client.openapi.JSON; 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; @@ -36,9 +35,7 @@ import java.util.Map; import java.util.Objects; 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; @@ -46,22 +43,21 @@ import org.jdrupes.vmoperator.util.DataPath; /** * Represents a VM definition. */ -@SuppressWarnings({ "PMD.DataClass", "PMD.TooManyMethods" }) -public class VmDefinition { +@SuppressWarnings({ "PMD.DataClass", "PMD.TooManyMethods", + "PMD.CouplingBetweenObjects" }) +public class VmDefinition extends K8sDynamicModel { - @SuppressWarnings("PMD.FieldNamingConventions") + @SuppressWarnings({ "PMD.FieldNamingConventions", "unused" }) private static final Logger logger = Logger.getLogger(VmDefinition.class.getName()); @SuppressWarnings("PMD.FieldNamingConventions") + private static final Gson gson = new JSON().getGson(); + @SuppressWarnings("PMD.FieldNamingConventions") private static final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule()); - private String kind; - private String apiVersion; - private V1ObjectMeta metadata; - private Map spec; - private Map status; - private final Map extra = new ConcurrentHashMap<>(); + private final Model model; + private VmExtraData extraData; /** * The VM state from the VM definition. @@ -145,66 +141,34 @@ public class VmDefinition { } /** - * Gets the kind. + * Instantiates a new vm definition. * - * @return the kind + * @param delegate the delegate + * @param json the json */ - public String getKind() { - return kind; + public VmDefinition(Gson delegate, JsonObject json) { + super(delegate, json); + model = gson.fromJson(json, Model.class); } /** - * Sets the kind. + * Gets the spec. * - * @param kind the kind to set + * @return the spec */ - public void setKind(String kind) { - this.kind = kind; + public Map spec() { + return model.getSpec(); } /** - * Gets the api version. + * Get a value from the spec using {@link DataPath#get}. * - * @return the apiVersion + * @param the generic type + * @param selectors the selectors + * @return the value, if found */ - public String getApiVersion() { - return apiVersion; - } - - /** - * Sets the api version. - * - * @param apiVersion the apiVersion to set - */ - public void setApiVersion(String apiVersion) { - this.apiVersion = apiVersion; - } - - /** - * Gets the metadata. - * - * @return the metadata - */ - public V1ObjectMeta getMetadata() { - return metadata; - } - - /** - * Gets the metadata. - * - * @return the metadata - */ - public V1ObjectMeta metadata() { - return metadata; - } - - /** - * Sets the metadata. - * - * @param metadata the metadata to set - */ - public void setMetadata(V1ObjectMeta metadata) { - this.metadata = metadata; + public Optional fromSpec(Object... selectors) { + return DataPath.get(spec(), selectors); } /** @@ -217,35 +181,6 @@ public class VmDefinition { .orElse(Collections.emptyList()); } - /** - * Gets the spec. - * - * @return the spec - */ - public Map getSpec() { - return spec; - } - - /** - * Gets the spec. - * - * @return the spec - */ - public Map spec() { - return spec; - } - - /** - * Get a value from the spec using {@link DataPath#get}. - * - * @param the generic type - * @param selectors the selectors - * @return the value, if found - */ - public Optional fromSpec(Object... selectors) { - return DataPath.get(spec, selectors); - } - /** * Get a value from the `spec().get("vm")` using {@link DataPath#get}. * @@ -254,35 +189,17 @@ public class VmDefinition { * @return the value, if found */ public Optional fromVm(Object... selectors) { - return DataPath.get(spec, "vm") + return DataPath.get(spec(), "vm") .flatMap(vm -> DataPath.get(vm, selectors)); } - /** - * Sets the spec. - * - * @param spec the spec to set - */ - public void setSpec(Map spec) { - this.spec = spec; - } - - /** - * Gets the status. - * - * @return the status - */ - public Map getStatus() { - return status; - } - /** * Gets the status. * * @return the status */ public Map status() { - return status; + return model.getStatus(); } /** @@ -293,16 +210,7 @@ public class VmDefinition { * @return the value, if found */ public Optional fromStatus(Object... selectors) { - return DataPath.get(status, selectors); - } - - /** - * Sets the status. - * - * @param status the status to set - */ - public void setStatus(Map status) { - this.status = status; + return DataPath.get(status(), selectors); } /** @@ -382,27 +290,21 @@ public class VmDefinition { } /** - * Set extra data (locally used, unknown to kubernetes). - * - * @param property the property - * @param value the value + * Set extra data (unknown to kubernetes). * @return the VM definition */ - public VmDefinition extra(String property, Object value) { - extra.put(property, value); + /* default */ VmDefinition extra(VmExtraData extraData) { + this.extraData = extraData; return this; } /** - * Return extra data. + * Return the extra data. * - * @param the generic type - * @param property the property - * @return the object + * @return the data */ - @SuppressWarnings("unchecked") - public T extra(String property) { - return (T) extra.get(property); + public Optional extra() { + return Optional.ofNullable(extraData); } /** @@ -411,7 +313,7 @@ public class VmDefinition { * @return the string */ public String name() { - return metadata.getName(); + return metadata().getName(); } /** @@ -420,7 +322,7 @@ public class VmDefinition { * @return the string */ public String namespace() { - return metadata.getNamespace(); + return metadata().getNamespace(); } /** @@ -490,78 +392,6 @@ 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. 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. 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 displayIp(Class preferredIpVersion) { - Optional 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.> 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. * @@ -569,7 +399,7 @@ public class VmDefinition { */ @Override public int hashCode() { - return Objects.hash(metadata.getNamespace(), metadata.getName()); + return Objects.hash(metadata().getNamespace(), metadata().getName()); } /** @@ -590,9 +420,55 @@ public class VmDefinition { return false; } VmDefinition other = (VmDefinition) obj; - return Objects.equals(metadata.getNamespace(), - other.metadata.getNamespace()) - && Objects.equals(metadata.getName(), other.metadata.getName()); + return Objects.equals(metadata().getNamespace(), + other.metadata().getNamespace()) + && Objects.equals(metadata().getName(), other.metadata().getName()); + } + + /** + * The Class Model. + */ + public static class Model { + + private Map spec; + private Map status; + + /** + * Gets the spec. + * + * @return the spec + */ + public Map getSpec() { + return spec; + } + + /** + * Sets the spec. + * + * @param spec the spec to set + */ + public void setSpec(Map spec) { + this.spec = spec; + } + + /** + * Gets the status. + * + * @return the status + */ + public Map getStatus() { + return status; + } + + /** + * Sets the status. + * + * @param status the status to set + */ + public void setStatus(Map status) { + this.status = status; + } + } } diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionStub.java index 49da3e0..72194da 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionStub.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionStub.java @@ -33,10 +33,10 @@ import java.util.Collection; */ @SuppressWarnings("PMD.DataflowAnomalyAnalysis") public class VmDefinitionStub - extends K8sDynamicStubBase { + extends K8sDynamicStubBase { - private static DynamicTypeAdapterFactory taf = new VmDefintionModelTypeAdapterFactory(); + private static DynamicTypeAdapterFactory taf = new VmDefintionModelTypeAdapterFactory(); /** * Instantiates a new stub for VM defintions. @@ -48,7 +48,7 @@ public class VmDefinitionStub */ public VmDefinitionStub(K8sClient client, APIResource context, String namespace, String name) { - super(VmDefinitionModel.class, VmDefinitionModels.class, taf, client, + super(VmDefinition.class, VmDefinitions.class, taf, client, context, namespace, name); } @@ -101,10 +101,10 @@ public class VmDefinitionStub */ public static VmDefinitionStub createFromYaml(K8sClient client, APIResource context, Reader yaml) throws ApiException { - var model = new VmDefinitionModel(client.getJSON().getGson(), + var model = new VmDefinition(client.getJSON().getGson(), K8s.yamlToJson(client, yaml)); - return K8sGenericStub.create(VmDefinitionModel.class, - VmDefinitionModels.class, client, context, model, + return K8sGenericStub.create(VmDefinition.class, + VmDefinitions.class, client, context, model, (c, ns, n) -> new VmDefinitionStub(c, context, ns, n)); } @@ -121,8 +121,8 @@ public class VmDefinitionStub public static Collection list(K8sClient client, APIResource context, String namespace, ListOptions options) throws ApiException { - return K8sGenericStub.list(VmDefinitionModel.class, - VmDefinitionModels.class, client, context, namespace, options, + return K8sGenericStub.list(VmDefinition.class, + VmDefinitions.class, client, context, namespace, options, (c, ns, n) -> new VmDefinitionStub(c, context, ns, n)); } @@ -144,13 +144,13 @@ public class VmDefinitionStub * A factory for creating VmDefinitionModel(s) objects. */ public static class VmDefintionModelTypeAdapterFactory extends - DynamicTypeAdapterFactory { + DynamicTypeAdapterFactory { /** * Instantiates a new dynamic model type adapter factory. */ public VmDefintionModelTypeAdapterFactory() { - super(VmDefinitionModel.class, VmDefinitionModels.class); + super(VmDefinition.class, VmDefinitions.class); } } diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionModels.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitions.java similarity index 79% rename from org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionModels.java rename to org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitions.java index 5ac412f..c79654e 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionModels.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitions.java @@ -22,10 +22,10 @@ import com.google.gson.Gson; import com.google.gson.JsonObject; /** - * Represents a list of {@link VmDefinitionModel}s. + * Represents a list of {@link VmDefinition}s. */ -public class VmDefinitionModels - extends K8sDynamicModelsBase { +public class VmDefinitions + extends K8sDynamicModelsBase { /** * Initialize the object list using the given JSON data. @@ -33,7 +33,7 @@ public class VmDefinitionModels * @param delegate the gson instance to use for extracting structured data * @param data the data */ - public VmDefinitionModels(Gson delegate, JsonObject data) { - super(VmDefinitionModel.class, delegate, data); + public VmDefinitions(Gson delegate, JsonObject data) { + super(VmDefinition.class, delegate, data); } } diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmExtraData.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmExtraData.java new file mode 100644 index 0000000..85913c2 --- /dev/null +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmExtraData.java @@ -0,0 +1,171 @@ +/* + * VM-Operator + * 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 + * 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 . + */ + +package org.jdrupes.vmoperator.common; + +import io.kubernetes.client.util.Strings; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents internally used dynamic data associated with a + * {@link VmDefinition}. + */ +public class VmExtraData { + + @SuppressWarnings("PMD.FieldNamingConventions") + private static final Logger logger + = Logger.getLogger(VmExtraData.class.getName()); + + private final VmDefinition vmDef; + private String nodeName = ""; + private List nodeAddresses = Collections.emptyList(); + private long resetCount; + + /** + * Initializes a new instance. + * + * @param vmDef the VM definition + */ + public VmExtraData(VmDefinition vmDef) { + this.vmDef = vmDef; + vmDef.extra(this); + } + + /** + * Sets the node info. + * + * @param name the name + * @param addresses the addresses + * @return the VM extra data + */ + public VmExtraData nodeInfo(String name, List addresses) { + nodeName = name; + nodeAddresses = addresses; + return this; + } + + /** + * Return the node name. + * + * @return the string + */ + public String nodeName() { + return nodeName; + } + + /** + * Sets the reset count. + * + * @param resetCount the reset count + * @return the vm extra data + */ + public VmExtraData resetCount(long resetCount) { + this.resetCount = resetCount; + return this; + } + + /** + * Returns the reset count. + * + * @return the long + */ + public long resetCount() { + return resetCount; + } + + /** + * 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 " + vmDef.name()); + return null; + } + var port = vmDef. fromVm("display", "spice", "port") + .map(Number::longValue); + if (port.isEmpty()) { + logger + .severe(() -> "No port defined for display of " + vmDef.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'); + } + vmDef. 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 displayIp(Class preferredIpVersion) { + Optional 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.metadata().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 = nodeAddresses.stream().map(a -> { + try { + return InetAddress.getByName(a); + } catch (UnknownHostException e) { + logger.warning(() -> "Invalid IP address: " + a); + return null; + } + }).filter(Objects::nonNull).toList(); + logger.fine( + () -> "Known IP addresses for " + vmDef.name() + ": " + addrs); + return addrs.stream() + .filter(a -> preferredIpVersion.isAssignableFrom(a.getClass())) + .findFirst().or(() -> addrs.stream().findFirst()); + } + +} diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java index cfc29ef..e0817d5 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java @@ -134,6 +134,78 @@ public class VmPool { return vms; } + /** + * Collect all permissions for the given user with the given roles. + * + * @param user the user + * @param roles the roles + * @return the sets the + */ + public Set permissionsFor(String user, + Collection roles) { + return permissions.stream() + .filter(g -> DataPath.get(g, "user").map(u -> u.equals(user)) + .orElse(false) + || DataPath.get(g, "role").map(roles::contains).orElse(false)) + .map(g -> DataPath.> get(g, "may") + .orElse(Collections.emptySet()).stream()) + .flatMap(Function.identity()).collect(Collectors.toSet()); + } + + /** + * Checks if the given VM belongs to the pool and is not in use. + * + * @param vmDef the vm def + * @return true, if is assignable + */ + @SuppressWarnings("PMD.SimplifyBooleanReturns") + public boolean isAssignable(VmDefinition vmDef) { + // Check if the VM is in the pool + if (!vmDef.pools().contains(name)) { + return false; + } + + // Check if the VM is not in use + if (vmDef.consoleConnected()) { + return false; + } + + // If not assigned, it's usable + if (vmDef.assignedTo().isEmpty()) { + return true; + } + + // Check if it is to be retained + if (vmDef.assignmentLastUsed() + .map(this::retainUntil) + .map(ru -> Instant.now().isBefore(ru)).orElse(false)) { + return false; + } + + // Additional check in case lastUsed has not been updated + // by PoolMonitor#onVmDefChanged() yet ("race condition") + if (vmDef.condition("ConsoleConnected") + .map(cc -> cc.getLastTransitionTime().toInstant()) + .map(this::retainUntil) + .map(ru -> Instant.now().isBefore(ru)).orElse(false)) { + return false; + } + return true; + } + + /** + * Return the instant until which an assignment should be retained. + * + * @param lastUsed the last used + * @return the instant + */ + public Instant retainUntil(Instant lastUsed) { + if (retention.startsWith("P")) { + return lastUsed.plus(Duration.parse(retention)); + } + return Instant.parse(retention); + } + /** * To string. * @@ -158,35 +230,4 @@ public class VmPool { builder.append(']'); return builder.toString(); } - - /** - * Collect all permissions for the given user with the given roles. - * - * @param user the user - * @param roles the roles - * @return the sets the - */ - public Set permissionsFor(String user, - Collection roles) { - return permissions.stream() - .filter(g -> DataPath.get(g, "user").map(u -> u.equals(user)) - .orElse(false) - || DataPath.get(g, "role").map(roles::contains).orElse(false)) - .map(g -> DataPath.> get(g, "may") - .orElse(Collections.emptySet()).stream()) - .flatMap(Function.identity()).collect(Collectors.toSet()); - } - - /** - * Return the instant until which an assignment should be retained. - * - * @param lastUsed the last used - * @return the instant - */ - public Instant retainUntil(Instant lastUsed) { - if (retention.startsWith("P")) { - return lastUsed.plus(Duration.parse(retention)); - } - return Instant.parse(retention); - } } diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionModel.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/UpdateAssignment.java similarity index 51% rename from org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionModel.java rename to org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/UpdateAssignment.java index d4ae5da..676af3d 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionModel.java +++ b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/UpdateAssignment.java @@ -16,24 +16,45 @@ * along with this program. If not, see . */ -package org.jdrupes.vmoperator.common; +package org.jdrupes.vmoperator.manager.events; -import com.google.gson.Gson; -import com.google.gson.JsonObject; +import org.jgrapes.core.Event; /** - * Represents a VM definition. + * Note the assignment to a user in the VM status. */ @SuppressWarnings("PMD.DataClass") -public class VmDefinitionModel extends K8sDynamicModel { +public class UpdateAssignment extends Event { + + private final String usedPool; + private final String toUser; /** - * Instantiates a new model from the JSON representation. + * Instantiates a new event. * - * @param delegate the gson instance to use for extracting structured data - * @param json the JSON + * @param usedPool the used pool + * @param toUser the to user */ - public VmDefinitionModel(Gson delegate, JsonObject json) { - super(delegate, json); + public UpdateAssignment(String usedPool, String toUser) { + this.usedPool = usedPool; + this.toUser = toUser; + } + + /** + * Gets the pool to assign from. + * + * @return the pool + */ + public String usedPool() { + return usedPool; + } + + /** + * Gets the user to assign to. + * + * @return the to user + */ + public String toUser() { + return toUser; } } diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml index 8a50a5e..f5aabc5 100644 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml +++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml @@ -10,7 +10,7 @@ metadata: annotations: vmoperator.jdrupes.org/version: ${ managerVersion } ownerReferences: - - apiVersion: ${ cr.apiVersion } + - apiVersion: ${ cr.apiVersion() } kind: ${ constants.VM_OP_KIND_VM } name: ${ cr.name() } uid: ${ cr.metadata().getUid() } @@ -53,7 +53,7 @@ data: # i.e. if you start the VM without a value for this property, and # decide to trigger a reset later, you have to first set the value # and then inrement it. - resetCounter: ${ cr.extra("resetCount")?c } + resetCounter: ${ cr.extra().get().resetCount()?c } # Forward the cloud-init data if provided <#if spec.cloudInit??> diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerLoadBalancer.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerLoadBalancer.ftl.yaml index 9a70e19..c25d7f4 100644 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerLoadBalancer.ftl.yaml +++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerLoadBalancer.ftl.yaml @@ -10,7 +10,7 @@ metadata: annotations: vmoperator.jdrupes.org/version: ${ managerVersion } ownerReferences: - - apiVersion: ${ cr.apiVersion } + - apiVersion: ${ cr.apiVersion() } kind: ${ constants.VM_OP_KIND_VM } name: ${ cr.name() } uid: ${ cr.metadata().getUid() } diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerPod.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerPod.ftl.yaml index e62ac70..917d790 100644 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerPod.ftl.yaml +++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerPod.ftl.yaml @@ -14,7 +14,7 @@ metadata: vmrunner.jdrupes.org/cmVersion: "${ cm.metadata.resourceVersion }" vmoperator.jdrupes.org/version: ${ managerVersion } ownerReferences: - - apiVersion: ${ cr.apiVersion } + - apiVersion: ${ cr.apiVersion() } kind: ${ constants.VM_OP_KIND_VM } name: ${ cr.name() } uid: ${ cr.metadata().getUid() } diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java index 1ef17f7..80ff0f7 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java @@ -18,23 +18,29 @@ package org.jdrupes.vmoperator.manager; +import com.google.gson.JsonObject; import io.kubernetes.client.apimachinery.GroupVersionKind; import io.kubernetes.client.custom.V1Patch; import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.Configuration; import java.io.IOException; +import java.net.HttpURLConnection; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Instant; import java.util.logging.Level; import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM; import org.jdrupes.vmoperator.common.K8sClient; import org.jdrupes.vmoperator.common.K8sDynamicStub; +import org.jdrupes.vmoperator.common.VmDefinitionStub; import org.jdrupes.vmoperator.manager.events.ChannelManager; import org.jdrupes.vmoperator.manager.events.Exit; import org.jdrupes.vmoperator.manager.events.ModifyVm; +import org.jdrupes.vmoperator.manager.events.UpdateAssignment; import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmDefChanged; +import org.jdrupes.vmoperator.util.GsonPtr; import org.jgrapes.core.Channel; import org.jgrapes.core.Component; import org.jgrapes.core.annotation.Handler; @@ -204,4 +210,41 @@ public class Controller extends Component { () -> "Cannot patch definition for Vm " + vmStub.name()); } } + + /** + * Attempt to Update the assignment information in the status of the + * VM CR. Returns true if successful. The handler does not attempt + * retries, because in case of failure it will be necessary to + * re-evaluate the chosen VM. + * + * @param event the event + * @param channel the channel + * @throws ApiException the api exception + */ + @Handler + public void onUpdatedAssignment(UpdateAssignment event, VmChannel channel) + throws ApiException { + try { + var vmDef = channel.vmDefinition(); + var vmStub = VmDefinitionStub.get(channel.client(), + new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM), + vmDef.namespace(), vmDef.name()); + if (vmStub.updateStatus(vmDef, from -> { + JsonObject status = from.statusJson(); + var assignment = GsonPtr.to(status).to("assignment"); + assignment.set("pool", event.usedPool()); + assignment.set("user", event.toUser()); + assignment.set("lastUsed", Instant.now().toString()); + return status; + }).isPresent()) { + event.setResult(true); + } + } catch (ApiException e) { + // Log exceptions except for conflict, which can be expected + if (HttpURLConnection.HTTP_CONFLICT != e.getCode()) { + throw e; + } + } + event.setResult(false); + } } diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretMonitor.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretMonitor.java index 141c806..a0809e9 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretMonitor.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretMonitor.java @@ -185,7 +185,7 @@ public class DisplaySecretMonitor new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM), event.vmDefinition().namespace(), event.vmDefinition().name()); vmStub.updateStatus(from -> { - JsonObject status = from.status(); + JsonObject status = from.statusJson(); status.addProperty("consoleUser", event.user()); return status; }); diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolMonitor.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolMonitor.java index b83c0f5..25fb10b 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolMonitor.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolMonitor.java @@ -187,7 +187,8 @@ public class PoolMonitor extends new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM), vmDef.namespace(), vmDef.name()); vmStub.updateStatus(from -> { - JsonObject status = from.status(); + // TODO + JsonObject status = from.statusJson(); var assignment = GsonPtr.to(status).to("assignment"); assignment.set("lastUsed", ccChange.get().toString()); return status; diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java index 641247a..7dbb410 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java @@ -248,7 +248,7 @@ public class Reconciler extends Component { public void onResetVm(ResetVm event, VmChannel channel) throws ApiException, IOException, TemplateException { var vmDef = channel.vmDefinition(); - vmDef.extra("resetCount", vmDef. extra("resetCount") + 1); + vmDef.extra().ifPresent(e -> e.resetCount(e.resetCount() + 1)); Map model = prepareModel(channel.client(), channel.vmDefinition()); cmReconciler.reconcile(model, channel); diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java index c2d717a..5c1ae77 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java @@ -18,8 +18,6 @@ package org.jdrupes.vmoperator.manager; -import com.google.gson.JsonObject; -import io.kubernetes.client.apimachinery.GroupVersionKind; import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.models.V1ObjectMeta; import io.kubernetes.client.util.Watch; @@ -43,9 +41,9 @@ import org.jdrupes.vmoperator.common.K8sV1ConfigMapStub; import org.jdrupes.vmoperator.common.K8sV1PodStub; import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub; import org.jdrupes.vmoperator.common.VmDefinition; -import org.jdrupes.vmoperator.common.VmDefinitionModel; -import org.jdrupes.vmoperator.common.VmDefinitionModels; import org.jdrupes.vmoperator.common.VmDefinitionStub; +import org.jdrupes.vmoperator.common.VmDefinitions; +import org.jdrupes.vmoperator.common.VmExtraData; import org.jdrupes.vmoperator.common.VmPool; import static org.jdrupes.vmoperator.manager.Constants.APP_NAME; import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME; @@ -55,9 +53,9 @@ 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.UpdateAssignment; import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmDefChanged; -import org.jdrupes.vmoperator.util.GsonPtr; import org.jgrapes.core.Channel; import org.jgrapes.core.Event; import org.jgrapes.core.annotation.Handler; @@ -67,7 +65,7 @@ import org.jgrapes.core.annotation.Handler; */ @SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports" }) public class VmMonitor extends - AbstractMonitor { + AbstractMonitor { private final ChannelManager channelManager; @@ -79,8 +77,8 @@ public class VmMonitor extends */ public VmMonitor(Channel componentChannel, ChannelManager channelManager) { - super(componentChannel, VmDefinitionModel.class, - VmDefinitionModels.class); + super(componentChannel, VmDefinition.class, + VmDefinitions.class); this.channelManager = channelManager; } @@ -124,7 +122,7 @@ public class VmMonitor extends @Override protected void handleChange(K8sClient client, - Watch.Response response) { + Watch.Response response) { V1ObjectMeta metadata = response.object.getMetadata(); AtomicBoolean toBeAdded = new AtomicBoolean(false); VmChannel channel = channelManager.channel(metadata.getName()) @@ -134,21 +132,17 @@ public class VmMonitor extends }); // Get full definition and associate with channel as backup - var vmModel = response.object; - if (vmModel.data() == null) { + var vmDef = response.object; + if (vmDef.data() == null) { // ADDED event does not provide data, see // https://github.com/kubernetes-client/java/issues/3215 - vmModel = getModel(client, vmModel); + vmDef = getModel(client, vmDef); } - VmDefinition vmDef = null; - if (vmModel.data() != null) { + if (vmDef.data() != null) { // New data, augment and save - vmDef = client.getJSON().getGson().fromJson(vmModel.data(), - VmDefinition.class); - addDynamicData(channel.client(), vmDef, channel.vmDefinition()); + addExtraData(channel.client(), vmDef, channel.vmDefinition()); channel.setVmDefinition(vmDef); - } - if (vmDef == null) { + } else { // Reuse cached (e.g. if deleted) vmDef = channel.vmDefinition(); } @@ -175,8 +169,7 @@ public class VmMonitor extends channel.pipeline().fire(chgEvt, channel); } - private VmDefinitionModel getModel(K8sClient client, - VmDefinitionModel vmDef) { + private VmDefinition getModel(K8sClient client, VmDefinition vmDef) { try { return VmDefinitionStub.get(client, context(), namespace(), vmDef.metadata().getName()).model().orElse(null); @@ -186,17 +179,14 @@ public class VmMonitor extends } @SuppressWarnings("PMD.AvoidDuplicateLiterals") - private void addDynamicData(K8sClient client, VmDefinition vmDef, + private void addExtraData(K8sClient client, VmDefinition vmDef, VmDefinition prevState) { - // Maintain (or initialize) the resetCount - vmDef.extra("resetCount", - Optional.ofNullable(prevState).map(d -> d.extra("resetCount")) - .orElse(0L)); + var extra = new VmExtraData(vmDef); - // Node information - // Add defaults in case the VM is not running - vmDef.extra("nodeName", ""); - vmDef.extra("nodeAddress", ""); + // Maintain (or initialize) the resetCount + extra.resetCount( + Optional.ofNullable(prevState).flatMap(VmDefinition::extra) + .map(VmExtraData::resetCount).orElse(0L)); // VM definition status changes before the pod terminates. // This results in pod information being shown for a stopped @@ -204,6 +194,8 @@ public class VmMonitor extends if (!vmDef.conditionStatus("Running").orElse(false)) { return; } + + // Get pod and extract node information. var podSearch = new ListOptions(); podSearch.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ",app.kubernetes.io/component=" + APP_NAME @@ -213,16 +205,15 @@ public class VmMonitor extends = K8sV1PodStub.list(client, namespace(), podSearch); for (var podStub : podList) { var nodeName = podStub.model().get().getSpec().getNodeName(); - vmDef.extra("nodeName", nodeName); - logger.fine(() -> "Added node name " + nodeName + logger.fine(() -> "Adding node name " + nodeName + " to VM info for " + vmDef.name()); @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") var addrs = new ArrayList(); podStub.model().get().getStatus().getPodIPs().stream() .map(ip -> ip.getIp()).forEach(addrs::add); - vmDef.extra("nodeAddresses", addrs); - logger.fine(() -> "Added node addresses " + addrs + logger.fine(() -> "Adding node addresses " + addrs + " to VM info for " + vmDef.name()); + extra.nodeInfo(nodeName, addrs); } } catch (ApiException e) { logger.log(Level.WARNING, e, @@ -264,62 +255,55 @@ public class VmMonitor extends @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") public void onAssignVm(AssignVm event) throws ApiException, InterruptedException { - VmPool vmPool = null; while (true) { // Search for existing assignment. - var assignedVm = channelManager.channels().stream() + var vmQuery = channelManager.channels().stream() .filter(c -> c.vmDefinition().assignedFrom() .map(p -> p.equals(event.fromPool())).orElse(false)) .filter(c -> c.vmDefinition().assignedTo() .map(u -> u.equals(event.toUser())).orElse(false)) .findFirst(); - if (assignedVm.isPresent()) { - var vmDef = assignedVm.get().vmDefinition(); - event.setResult(new VmData(vmDef, assignedVm.get())); + if (vmQuery.isPresent()) { + var vmDef = vmQuery.get().vmDefinition(); + event.setResult(new VmData(vmDef, vmQuery.get())); return; } - // Get the pool definition for retention time calculations + // Get the pool definition for checking possible assignment + VmPool vmPool = newEventPipeline().fire(new GetPools() + .withName(event.fromPool())).get().stream().findFirst() + .orElse(null); if (vmPool == null) { - vmPool = newEventPipeline().fire(new GetPools() - .withName(event.fromPool())).get().stream().findFirst() - .orElse(null); - if (vmPool == null) { - return; - } + return; } // Find available VM. - var pool = vmPool; - assignedVm = channelManager.channels().stream() - .filter(c -> isAssignable(pool, c.vmDefinition())) + vmQuery = channelManager.channels().stream() + .filter(c -> vmPool.isAssignable(c.vmDefinition())) .sorted(Comparator.comparing((VmChannel c) -> c.vmDefinition() .assignmentLastUsed().orElse(Instant.ofEpochSecond(0))) .thenComparing(preferRunning)) .findFirst(); // None found - if (assignedVm.isEmpty()) { + if (vmQuery.isEmpty()) { return; } // Assign to user - var vmDef = assignedVm.get().vmDefinition(); - var vmStub = VmDefinitionStub.get(client(), - new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM), - vmDef.namespace(), vmDef.name()); - vmStub.updateStatus(from -> { - JsonObject status = from.status(); - var assignment = GsonPtr.to(status).to("assignment"); - assignment.set("pool", event.fromPool()); - assignment.set("user", event.toUser()); - assignment.set("lastUsed", Instant.now().toString()); - return status; - }); + var chosenVm = vmQuery.get(); + var vmPipeline = chosenVm.pipeline(); + if (Optional.ofNullable(vmPipeline.fire(new UpdateAssignment( + vmPool.name(), event.toUser()), chosenVm).get()) + .orElse(false)) { + var vmDef = chosenVm.vmDefinition(); + event.setResult(new VmData(vmDef, chosenVm)); - // Make sure that a newly assigned VM is running. - fire(new ModifyVm(vmDef.name(), "state", "Running", - assignedVm.get())); + // Make sure that a newly assigned VM is running. + chosenVm.pipeline().fire(new ModifyVm(vmDef.name(), + "state", "Running", chosenVm)); + return; + } } } @@ -336,38 +320,4 @@ public class VmMonitor extends } }; - @SuppressWarnings("PMD.SimplifyBooleanReturns") - private boolean isAssignable(VmPool pool, VmDefinition vmDef) { - // Check if the VM is in the pool - if (!vmDef.pools().contains(pool.name())) { - return false; - } - - // Check if the VM is not in use - if (vmDef.consoleConnected()) { - return false; - } - - // If not assigned, it's usable - if (vmDef.assignedTo().isEmpty()) { - return true; - } - - // Check if it is to be retained - if (vmDef.assignmentLastUsed() - .map(lu -> pool.retainUntil(lu)) - .map(ru -> Instant.now().isBefore(ru)).orElse(false)) { - return false; - } - - // Additional check in case lastUsed has not been updated - // by PoolMonitor#onVmDefChanged() yet ("race condition") - if (vmDef.condition("ConsoleConnected") - .map(cc -> cc.getLastTransitionTime().toInstant()) - .map(t -> pool.retainUntil(t)) - .map(ru -> Instant.now().isBefore(ru)).orElse(false)) { - return false; - } - return true; - } } diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/ConsoleTracker.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/ConsoleTracker.java index f2309df..b91b5df 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/ConsoleTracker.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/ConsoleTracker.java @@ -106,7 +106,7 @@ public class ConsoleTracker extends VmDefUpdater { mainChannelClientHost = event.clientHost(); mainChannelClientPort = event.clientPort(); vmStub.updateStatus(from -> { - JsonObject status = from.status(); + JsonObject status = from.statusJson(); status.addProperty("consoleClient", event.clientHost()); updateCondition(from, status, "ConsoleConnected", true, "Connected", "Connection from " + event.clientHost()); @@ -141,7 +141,7 @@ public class ConsoleTracker extends VmDefUpdater { return; } vmStub.updateStatus(from -> { - JsonObject status = from.status(); + JsonObject status = from.statusJson(); status.addProperty("consoleClient", ""); updateCondition(from, status, "ConsoleConnected", false, "Disconnected", event.clientHost() + " has disconnected"); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java index 0b18df0..d33358b 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java @@ -33,7 +33,7 @@ import static org.jdrupes.vmoperator.common.Constants.APP_NAME; import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM; import org.jdrupes.vmoperator.common.K8s; -import org.jdrupes.vmoperator.common.VmDefinitionModel; +import org.jdrupes.vmoperator.common.VmDefinition; import org.jdrupes.vmoperator.common.VmDefinitionStub; import org.jdrupes.vmoperator.runner.qemu.events.BalloonChangeEvent; import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu; @@ -140,12 +140,12 @@ public class StatusUpdater extends VmDefUpdater { if (vmDef.isPresent() && vmDef.get().metadata().getGeneration() == observedGeneration && (event.configuration().hasDisplayPassword - || vmDef.get().status().getAsJsonPrimitive( + || vmDef.get().statusJson().getAsJsonPrimitive( "displayPasswordSerial").getAsInt() == -1)) { return; } vmStub.updateStatus(vmDef.get(), from -> { - JsonObject status = from.status(); + JsonObject status = from.statusJson(); if (!event.configuration().hasDisplayPassword) { status.addProperty("displayPasswordSerial", -1); } @@ -169,14 +169,14 @@ public class StatusUpdater extends VmDefUpdater { "PMD.AvoidLiteralsInIfCondition" }) public void onRunnerStateChanged(RunnerStateChange event) throws ApiException { - VmDefinitionModel vmDef; + VmDefinition vmDef; if (vmStub == null || (vmDef = vmStub.model().orElse(null)) == null) { return; } vmStub.updateStatus(vmDef, from -> { - JsonObject status = from.status(); + JsonObject status = from.statusJson(); boolean running = RUNNING_STATES.contains(event.runState()); - updateCondition(vmDef, vmDef.status(), "Running", running, + updateCondition(vmDef, vmDef.statusJson(), "Running", running, event.reason(), event.message()); if (event.runState() == RunState.STARTING) { status.addProperty("ram", GsonPtr.to(from.data()) @@ -230,7 +230,7 @@ public class StatusUpdater extends VmDefUpdater { return; } vmStub.updateStatus(from -> { - JsonObject status = from.status(); + JsonObject status = from.statusJson(); status.addProperty("ram", new Quantity(new BigDecimal(event.size()), Format.BINARY_SI) .toSuffixedString()); @@ -250,7 +250,7 @@ public class StatusUpdater extends VmDefUpdater { return; } vmStub.updateStatus(from -> { - JsonObject status = from.status(); + JsonObject status = from.statusJson(); status.addProperty("cpus", event.usedCpus().size()); return status; }); @@ -269,7 +269,7 @@ public class StatusUpdater extends VmDefUpdater { return; } vmStub.updateStatus(from -> { - JsonObject status = from.status(); + JsonObject status = from.statusJson(); status.addProperty("displayPasswordSerial", status.get("displayPasswordSerial").getAsLong() + 1); return status; diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/VmDefUpdater.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/VmDefUpdater.java index 9683457..f04b478 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/VmDefUpdater.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/VmDefUpdater.java @@ -31,7 +31,7 @@ import java.util.Optional; import java.util.logging.Level; import java.util.stream.Collectors; import org.jdrupes.vmoperator.common.K8sClient; -import org.jdrupes.vmoperator.common.VmDefinitionModel; +import org.jdrupes.vmoperator.common.VmDefinition; import org.jdrupes.vmoperator.runner.qemu.events.Exit; import org.jgrapes.core.Channel; import org.jgrapes.core.Component; @@ -118,7 +118,7 @@ public class VmDefUpdater extends Component { * @param reason the reason for the change * @param message the message */ - protected void updateCondition(VmDefinitionModel from, JsonObject status, + protected void updateCondition(VmDefinition from, JsonObject status, String type, boolean state, String reason, String message) { // Optimize, as we can get this several times var current = status.getAsJsonArray("conditions").asList().stream() diff --git a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/VmAccess.java b/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/VmAccess.java index bd9a802..e283504 100644 --- a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/VmAccess.java +++ b/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/VmAccess.java @@ -593,7 +593,7 @@ public class VmAccess extends FreeMarkerConlet { Map.of("namespace", vmDef.namespace(), "name", vmDef.name()), "spec", vmDef.spec(), - "status", vmDef.getStatus()); + "status", vmDef.status()); } catch (JsonSyntaxException e) { logger.log(Level.SEVERE, e, () -> "Failed to serialize VM definition"); @@ -810,13 +810,12 @@ public class VmAccess extends FreeMarkerConlet { } 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)); + vmDef.extra() + .map(xtra -> xtra.connectionFile(e.password().orElse(null), + preferredIpVersion, deleteConnectionFile)) + .ifPresent( + cf -> channel.respond(new NotifyConletView(type(), + model.getConletId(), "openConsole", cf))); }); fire(pwQuery, vmChannel); } diff --git a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/VmMgmt.java b/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/VmMgmt.java index 819a0d4..4cc63fa 100644 --- a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/VmMgmt.java +++ b/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/VmMgmt.java @@ -41,6 +41,7 @@ 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.common.VmExtraData; import org.jdrupes.vmoperator.manager.events.ChannelTracker; import org.jdrupes.vmoperator.manager.events.GetDisplayPassword; import org.jdrupes.vmoperator.manager.events.ModifyVm; @@ -252,7 +253,7 @@ public class VmMgmt extends FreeMarkerConlet { "name", vmDef.name()), "spec", spec, "status", status, - "nodeName", vmDef.extra("nodeName"), + "nodeName", vmDef.extra().map(VmExtraData::nodeName).orElse(""), "permissions", vmDef.permissionsFor(user, roles).stream() .map(VmDefinition.Permission::toString).toList()); } @@ -484,13 +485,11 @@ public class VmMgmt extends FreeMarkerConlet { } 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)); + vmDef.extra().map(xtra -> xtra.connectionFile( + e.password().orElse(null), preferredIpVersion, + deleteConnectionFile)).ifPresent( + cf -> channel.respond(new NotifyConletView(type(), + model.getConletId(), "openConsole", cf))); }); fire(pwQuery, vmChannel); }