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