From d27339b1e9cdc3ee23a02e53078a808846ba2024 Mon Sep 17 00:00:00 2001 From: "Michael N. Lipp" Date: Sun, 2 Feb 2025 13:54:10 +0100 Subject: [PATCH] Make VM extra data a class. --- .../vmoperator/common/VmDefinition.java | 101 +---------- .../vmoperator/common/VmExtraData.java | 171 ++++++++++++++++++ .../vmoperator/manager/runnerConfig.ftl.yaml | 2 +- .../vmoperator/manager/Reconciler.java | 2 +- .../jdrupes/vmoperator/manager/VmMonitor.java | 27 ++- .../jdrupes/vmoperator/vmaccess/VmAccess.java | 13 +- .../org/jdrupes/vmoperator/vmmgmt/VmMgmt.java | 15 +- 7 files changed, 208 insertions(+), 123 deletions(-) create mode 100644 org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmExtraData.java 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 e677642..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 @@ -24,9 +24,6 @@ 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.util.Strings; -import java.net.InetAddress; -import java.net.UnknownHostException; import java.time.Instant; import java.util.Collection; import java.util.Collections; @@ -38,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; @@ -52,7 +47,7 @@ import org.jdrupes.vmoperator.util.DataPath; "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") @@ -62,7 +57,7 @@ public class VmDefinition extends K8sDynamicModel { = new ObjectMapper().registerModule(new JavaTimeModule()); private final Model model; - private final Map extra = new ConcurrentHashMap<>(); + private VmExtraData extraData; /** * The VM state from the VM definition. @@ -295,27 +290,21 @@ public class VmDefinition extends K8sDynamicModel { } /** - * 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); } /** @@ -403,78 +392,6 @@ public class VmDefinition extends K8sDynamicModel { .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. * 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.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml index f348244..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 @@ -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/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 17e3c58..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 @@ -43,6 +43,7 @@ import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub; import org.jdrupes.vmoperator.common.VmDefinition; 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; @@ -139,7 +140,7 @@ public class VmMonitor extends } if (vmDef.data() != null) { // New data, augment and save - addDynamicData(channel.client(), vmDef, channel.vmDefinition()); + addExtraData(channel.client(), vmDef, channel.vmDefinition()); channel.setVmDefinition(vmDef); } else { // Reuse cached (e.g. if deleted) @@ -178,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 @@ -196,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 @@ -205,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, 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 d6c385e..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 @@ -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); }