001/* 002 * VM-Operator 003 * Copyright (C) 2025 Michael N. Lipp 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <https://www.gnu.org/licenses/>. 017 */ 018 019package org.jdrupes.vmoperator.common; 020 021import io.kubernetes.client.util.Strings; 022import java.net.InetAddress; 023import java.net.UnknownHostException; 024import java.util.Collections; 025import java.util.List; 026import java.util.Objects; 027import java.util.Optional; 028import java.util.logging.Level; 029import java.util.logging.Logger; 030 031/** 032 * Represents internally used dynamic data associated with a 033 * {@link VmDefinition}. 034 */ 035public class VmExtraData { 036 037 @SuppressWarnings("PMD.FieldNamingConventions") 038 private static final Logger logger 039 = Logger.getLogger(VmExtraData.class.getName()); 040 041 private final VmDefinition vmDef; 042 private String nodeName = ""; 043 private List<String> nodeAddresses = Collections.emptyList(); 044 private long resetCount; 045 046 /** 047 * Initializes a new instance. 048 * 049 * @param vmDef the VM definition 050 */ 051 public VmExtraData(VmDefinition vmDef) { 052 this.vmDef = vmDef; 053 vmDef.extra(this); 054 } 055 056 /** 057 * Sets the node info. 058 * 059 * @param name the name 060 * @param addresses the addresses 061 * @return the VM extra data 062 */ 063 public VmExtraData nodeInfo(String name, List<String> addresses) { 064 nodeName = name; 065 nodeAddresses = addresses; 066 return this; 067 } 068 069 /** 070 * Return the node name. 071 * 072 * @return the string 073 */ 074 public String nodeName() { 075 return nodeName; 076 } 077 078 /** 079 * Gets the node addresses. 080 * 081 * @return the nodeAddresses 082 */ 083 public List<String> nodeAddresses() { 084 return nodeAddresses; 085 } 086 087 /** 088 * Sets the reset count. 089 * 090 * @param resetCount the reset count 091 * @return the vm extra data 092 */ 093 public VmExtraData resetCount(long resetCount) { 094 this.resetCount = resetCount; 095 return this; 096 } 097 098 /** 099 * Returns the reset count. 100 * 101 * @return the long 102 */ 103 public long resetCount() { 104 return resetCount; 105 } 106 107 /** 108 * Create a connection file. 109 * 110 * @param password the password 111 * @param preferredIpVersion the preferred IP version 112 * @param deleteConnectionFile the delete connection file 113 * @return the string 114 */ 115 public Optional<String> connectionFile(String password, 116 Class<?> preferredIpVersion, boolean deleteConnectionFile) { 117 var addr = displayIp(preferredIpVersion); 118 if (addr.isEmpty()) { 119 logger 120 .severe(() -> "Failed to find display IP for " + vmDef.name()); 121 return Optional.empty(); 122 } 123 var port = vmDef.<Number> fromVm("display", "spice", "port") 124 .map(Number::longValue); 125 if (port.isEmpty()) { 126 logger 127 .severe(() -> "No port defined for display of " + vmDef.name()); 128 return Optional.empty(); 129 } 130 StringBuffer data = new StringBuffer(100) 131 .append("[virt-viewer]\ntype=spice\nhost=") 132 .append(addr.get().getHostAddress()).append("\nport=") 133 .append(port.get().toString()) 134 .append('\n'); 135 if (password != null) { 136 data.append("password=").append(password).append('\n'); 137 } 138 vmDef.<String> fromVm("display", "spice", "proxyUrl") 139 .ifPresent(u -> { 140 if (!Strings.isNullOrEmpty(u)) { 141 data.append("proxy=").append(u).append('\n'); 142 } 143 }); 144 if (deleteConnectionFile) { 145 data.append("delete-this-file=1\n"); 146 } 147 return Optional.of(data.toString()); 148 } 149 150 private Optional<InetAddress> displayIp(Class<?> preferredIpVersion) { 151 Optional<String> server = vmDef.fromVm("display", "spice", "server"); 152 if (server.isPresent()) { 153 var srv = server.get(); 154 try { 155 var addr = InetAddress.getByName(srv); 156 logger.fine(() -> "Using IP address from CRD for " 157 + vmDef.metadata().getName() + ": " + addr); 158 return Optional.of(addr); 159 } catch (UnknownHostException e) { 160 logger.log(Level.SEVERE, e, () -> "Invalid server address " 161 + srv + ": " + e.getMessage()); 162 return Optional.empty(); 163 } 164 } 165 var addrs = nodeAddresses.stream().map(a -> { 166 try { 167 return InetAddress.getByName(a); 168 } catch (UnknownHostException e) { 169 logger.warning(() -> "Invalid IP address: " + a); 170 return null; 171 } 172 }).filter(Objects::nonNull).toList(); 173 logger.fine( 174 () -> "Known IP addresses for " + vmDef.name() + ": " + addrs); 175 return addrs.stream() 176 .filter(a -> preferredIpVersion.isAssignableFrom(a.getClass())) 177 .findFirst().or(() -> addrs.stream().findFirst()); 178 } 179 180}