001/* 002 * VM-Operator 003 * Copyright (C) 2023 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.runner.qemu; 020 021import java.io.IOException; 022import java.math.BigInteger; 023import java.nio.charset.StandardCharsets; 024import java.nio.file.Files; 025import java.nio.file.Path; 026import java.nio.file.attribute.PosixFilePermission; 027import java.time.Instant; 028import java.util.HashMap; 029import java.util.Map; 030import java.util.Set; 031import java.util.UUID; 032import java.util.logging.Level; 033import java.util.logging.Logger; 034import static org.jdrupes.vmoperator.common.Constants.APP_NAME; 035import org.jdrupes.vmoperator.common.Convertions; 036import org.jdrupes.vmoperator.util.Dto; 037import org.jdrupes.vmoperator.util.FsdUtils; 038 039/** 040 * The configuration information from the configuration file. 041 */ 042public class Configuration implements Dto { 043 private static final String CI_INSTANCE_ID = "instance-id"; 044 045 protected final Logger logger = Logger.getLogger(getClass().getName()); 046 047 /** Configuration timestamp. */ 048 public Instant asOf; 049 050 /** The data dir. */ 051 public Path dataDir; 052 053 /** The runtime dir. */ 054 public Path runtimeDir; 055 056 /** The template. */ 057 public String template; 058 059 /** The update template. */ 060 public boolean updateTemplate; 061 062 /** The swtpm socket. */ 063 public Path swtpmSocket; 064 065 /** The monitor socket. */ 066 public Path monitorSocket; 067 068 /** The firmware rom. */ 069 public Path firmwareRom; 070 071 /** The firmware vars. */ 072 public Path firmwareVars; 073 074 /** The display password. */ 075 public boolean hasDisplayPassword; 076 077 /** Optional cloud-init data. */ 078 public CloudInit cloudInit; 079 080 /** If guest shutdown changes CRD .vm.state to "Stopped". */ 081 public boolean guestShutdownStops; 082 083 /** Increments of the reset counter trigger a reset of the VM. */ 084 public Integer resetCounter; 085 086 /** The vm. */ 087 @SuppressWarnings("PMD.ShortVariable") 088 public Vm vm; 089 090 /** 091 * Subsection "cloud-init". 092 */ 093 public static class CloudInit implements Dto { 094 095 /** The meta data. */ 096 public Map<String, Object> metaData; 097 098 /** The user data. */ 099 public Map<String, Object> userData; 100 101 /** The network config. */ 102 public Map<String, Object> networkConfig; 103 } 104 105 /** 106 * Subsection "vm". 107 */ 108 @SuppressWarnings({ "PMD.ShortClassName", "PMD.TooManyFields", 109 "PMD.DataClass", "PMD.AvoidDuplicateLiterals" }) 110 public static class Vm implements Dto { 111 112 /** The name. */ 113 public String name; 114 115 /** The uuid. */ 116 public String uuid; 117 118 /** The use tpm. */ 119 public boolean useTpm; 120 121 /** The boot menu. */ 122 public boolean bootMenu; 123 124 /** The firmware. */ 125 public String firmware = "uefi"; 126 127 /** The maximum ram. */ 128 public BigInteger maximumRam; 129 130 /** The current ram. */ 131 public BigInteger currentRam; 132 133 /** The cpu model. */ 134 public String cpuModel = "host"; 135 136 /** The maximum cpus. */ 137 public int maximumCpus = 1; 138 139 /** The current cpus. */ 140 public int currentCpus = 1; 141 142 /** The cpu sockets. */ 143 public int sockets; 144 145 /** The dies per socket. */ 146 public int diesPerSocket; 147 148 /** The cores per die. */ 149 public int coresPerDie; 150 151 /** The threads per core. */ 152 public int threadsPerCore; 153 154 /** The accelerator. */ 155 public String accelerator = "kvm"; 156 157 /** The rtc base. */ 158 public String rtcBase = "utc"; 159 160 /** The rtc clock. */ 161 public String rtcClock = "rt"; 162 163 /** The powerdown timeout. */ 164 public int powerdownTimeout = 900; 165 166 /** The network. */ 167 public Network[] network = { new Network() }; 168 169 /** The drives. */ 170 public Drive[] drives = new Drive[0]; 171 172 /** The display. */ 173 public Display display; 174 175 /** 176 * Convert value from JSON parser. 177 * 178 * @param value the new maximum ram 179 */ 180 public void setMaximumRam(String value) { 181 maximumRam = Convertions.parseMemory(value); 182 } 183 184 /** 185 * Convert value from JSON parser. 186 * 187 * @param value the new current ram 188 */ 189 public void setCurrentRam(String value) { 190 currentRam = Convertions.parseMemory(value); 191 } 192 } 193 194 /** 195 * Subsection "network". 196 */ 197 @SuppressWarnings("PMD.DataClass") 198 public static class Network implements Dto { 199 200 /** The type. */ 201 public String type = "tap"; 202 203 /** The bridge. */ 204 public String bridge; 205 206 /** The device. */ 207 public String device = "virtio-net"; 208 209 /** The mac. */ 210 public String mac; 211 212 /** The net. */ 213 public String net; 214 } 215 216 /** 217 * Subsection "drive". 218 */ 219 @SuppressWarnings("PMD.DataClass") 220 public static class Drive implements Dto { 221 222 /** The type. */ 223 public String type; 224 225 /** The bootindex. */ 226 public Integer bootindex; 227 228 /** The device. */ 229 public String device; 230 231 /** The file. */ 232 public String file; 233 234 /** The resource. */ 235 public String resource; 236 } 237 238 /** 239 * The Class Display. 240 */ 241 public static class Display implements Dto { 242 243 /** The number of outputs. */ 244 public int outputs = 1; 245 246 /** The logged in user. */ 247 public String loggedInUser; 248 249 /** The spice. */ 250 public Spice spice; 251 } 252 253 /** 254 * Subsection "spice". 255 */ 256 @SuppressWarnings("PMD.DataClass") 257 public static class Spice implements Dto { 258 259 /** The port. */ 260 public int port = 5900; 261 262 /** The ticket. */ 263 public String ticket; 264 265 /** The streaming video. */ 266 public String streamingVideo; 267 268 /** The usb redirects. */ 269 public int usbRedirects = 2; 270 } 271 272 /** 273 * Check configuration. 274 * 275 * @return true, if successful 276 */ 277 public boolean check() { 278 if (vm == null || vm.name == null) { 279 logger.severe(() -> "Configuration is missing mandatory entries."); 280 return false; 281 } 282 if (!checkRuntimeDir() || !checkDataDir() || !checkUuid()) { 283 return false; 284 } 285 286 // Adjust max cpus if necessary 287 if (vm.currentCpus > vm.maximumCpus) { 288 vm.maximumCpus = vm.currentCpus; 289 } 290 291 checkDrives(); 292 checkCloudInit(); 293 294 return true; 295 } 296 297 private void checkDrives() { 298 for (Drive drive : vm.drives) { 299 if (drive.file != null || drive.device != null 300 || "ide-cd".equals(drive.type)) { 301 continue; 302 } 303 if (drive.resource == null) { 304 logger.severe( 305 () -> "Drive configuration is missing its resource."); 306 307 } 308 if (Files.isRegularFile(Path.of(drive.resource))) { 309 drive.file = drive.resource; 310 } else { 311 drive.device = drive.resource; 312 } 313 } 314 } 315 316 private boolean checkRuntimeDir() { 317 // Runtime directory (sockets etc.) 318 if (runtimeDir == null) { 319 var appDir = FsdUtils.runtimeDir(APP_NAME.replace("-", "")); 320 if (!Files.exists(appDir) && appDir.toFile().mkdirs()) { 321 try { 322 // When appDir is derived from XDG_RUNTIME_DIR 323 // the latter should already have these permissions, 324 // but let's be on the safe side. 325 Files.setPosixFilePermissions(appDir, 326 Set.of(PosixFilePermission.OWNER_READ, 327 PosixFilePermission.OWNER_WRITE, 328 PosixFilePermission.OWNER_EXECUTE)); 329 } catch (IOException e) { 330 logger.warning(() -> String.format( 331 "Cannot set permissions rwx------ on \"%s\".", 332 runtimeDir)); 333 } 334 } 335 runtimeDir = FsdUtils.runtimeDir(APP_NAME.replace("-", "")) 336 .resolve(vm.name); 337 runtimeDir.toFile().mkdir(); 338 swtpmSocket = runtimeDir.resolve("swtpm-sock"); 339 monitorSocket = runtimeDir.resolve("monitor.sock"); 340 } 341 if (!Files.isDirectory(runtimeDir) || !Files.isWritable(runtimeDir)) { 342 logger.severe(() -> String.format( 343 "Configured runtime directory \"%s\"" 344 + " does not exist or isn't writable.", 345 runtimeDir)); 346 return false; 347 } 348 return true; 349 } 350 351 private boolean checkDataDir() { 352 // Data directory 353 if (dataDir == null) { 354 dataDir 355 = FsdUtils.dataHome(APP_NAME.replace("-", "")).resolve(vm.name); 356 } 357 if (!Files.exists(dataDir)) { 358 dataDir.toFile().mkdirs(); 359 } 360 if (!Files.isDirectory(dataDir) || !Files.isWritable(dataDir)) { 361 logger.severe(() -> String.format( 362 "Configured data directory \"%s\"" 363 + " does not exist or isn't writable.", 364 dataDir)); 365 return false; 366 } 367 return true; 368 } 369 370 private boolean checkUuid() { 371 // Explicitly configured uuid takes precedence. 372 if (vm.uuid != null) { 373 return true; 374 } 375 376 // Try to read stored uuid. 377 Path uuidPath = dataDir.resolve("uuid.txt"); 378 if (Files.isReadable(uuidPath)) { 379 try { 380 var stored 381 = Files.lines(uuidPath, StandardCharsets.UTF_8).findFirst(); 382 if (stored.isPresent()) { 383 vm.uuid = stored.get(); 384 return true; 385 } 386 } catch (IOException e) { 387 logger.log(Level.WARNING, e, 388 () -> "Stored uuid cannot be read: " + e.getMessage()); 389 } 390 } 391 392 // Generate new uuid 393 vm.uuid = UUID.randomUUID().toString(); 394 try { 395 Files.writeString(uuidPath, vm.uuid + "\n"); 396 } catch (IOException e) { 397 logger.log(Level.WARNING, e, 398 () -> "Cannot store uuid: " + e.getMessage()); 399 } 400 401 return true; 402 } 403 404 private void checkCloudInit() { 405 if (cloudInit == null) { 406 return; 407 } 408 409 // Provide default for instance-id 410 if (cloudInit.metaData == null) { 411 cloudInit.metaData = new HashMap<>(); 412 } 413 if (!cloudInit.metaData.containsKey(CI_INSTANCE_ID)) { 414 cloudInit.metaData.put(CI_INSTANCE_ID, "v" + asOf.getEpochSecond()); 415 } 416 } 417}