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 * Sets the reset count. 080 * 081 * @param resetCount the reset count 082 * @return the vm extra data 083 */ 084 public VmExtraData resetCount(long resetCount) { 085 this.resetCount = resetCount; 086 return this; 087 } 088 089 /** 090 * Returns the reset count. 091 * 092 * @return the long 093 */ 094 public long resetCount() { 095 return resetCount; 096 } 097 098 /** 099 * Create a connection file. 100 * 101 * @param password the password 102 * @param preferredIpVersion the preferred IP version 103 * @param deleteConnectionFile the delete connection file 104 * @return the string 105 */ 106 public String connectionFile(String password, 107 Class<?> preferredIpVersion, boolean deleteConnectionFile) { 108 var addr = displayIp(preferredIpVersion); 109 if (addr.isEmpty()) { 110 logger 111 .severe(() -> "Failed to find display IP for " + vmDef.name()); 112 return null; 113 } 114 var port = vmDef.<Number> fromVm("display", "spice", "port") 115 .map(Number::longValue); 116 if (port.isEmpty()) { 117 logger 118 .severe(() -> "No port defined for display of " + vmDef.name()); 119 return null; 120 } 121 StringBuffer data = new StringBuffer(100) 122 .append("[virt-viewer]\ntype=spice\nhost=") 123 .append(addr.get().getHostAddress()).append("\nport=") 124 .append(port.get().toString()) 125 .append('\n'); 126 if (password != null) { 127 data.append("password=").append(password).append('\n'); 128 } 129 vmDef.<String> fromVm("display", "spice", "proxyUrl") 130 .ifPresent(u -> { 131 if (!Strings.isNullOrEmpty(u)) { 132 data.append("proxy=").append(u).append('\n'); 133 } 134 }); 135 if (deleteConnectionFile) { 136 data.append("delete-this-file=1\n"); 137 } 138 return data.toString(); 139 } 140 141 private Optional<InetAddress> displayIp(Class<?> preferredIpVersion) { 142 Optional<String> server = vmDef.fromVm("display", "spice", "server"); 143 if (server.isPresent()) { 144 var srv = server.get(); 145 try { 146 var addr = InetAddress.getByName(srv); 147 logger.fine(() -> "Using IP address from CRD for " 148 + vmDef.metadata().getName() + ": " + addr); 149 return Optional.of(addr); 150 } catch (UnknownHostException e) { 151 logger.log(Level.SEVERE, e, () -> "Invalid server address " 152 + srv + ": " + e.getMessage()); 153 return Optional.empty(); 154 } 155 } 156 var addrs = nodeAddresses.stream().map(a -> { 157 try { 158 return InetAddress.getByName(a); 159 } catch (UnknownHostException e) { 160 logger.warning(() -> "Invalid IP address: " + a); 161 return null; 162 } 163 }).filter(Objects::nonNull).toList(); 164 logger.fine( 165 () -> "Known IP addresses for " + vmDef.name() + ": " + addrs); 166 return addrs.stream() 167 .filter(a -> preferredIpVersion.isAssignableFrom(a.getClass())) 168 .findFirst().or(() -> addrs.stream().findFirst()); 169 } 170 171}