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 com.fasterxml.jackson.databind.ObjectMapper; 022import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 023import com.google.gson.Gson; 024import com.google.gson.JsonObject; 025import io.kubernetes.client.openapi.JSON; 026import io.kubernetes.client.openapi.models.V1Condition; 027import java.time.Instant; 028import java.util.Collection; 029import java.util.Collections; 030import java.util.EnumSet; 031import java.util.HashMap; 032import java.util.HashSet; 033import java.util.List; 034import java.util.Map; 035import java.util.Objects; 036import java.util.Optional; 037import java.util.Set; 038import java.util.function.Function; 039import java.util.logging.Logger; 040import java.util.stream.Collectors; 041import org.jdrupes.vmoperator.common.Constants.Status; 042import org.jdrupes.vmoperator.util.DataPath; 043 044/** 045 * Represents a VM definition. 046 */ 047@SuppressWarnings({ "PMD.DataClass", "PMD.TooManyMethods", 048 "PMD.CouplingBetweenObjects" }) 049public class VmDefinition extends K8sDynamicModel { 050 051 @SuppressWarnings({ "PMD.FieldNamingConventions", "unused" }) 052 private static final Logger logger 053 = Logger.getLogger(VmDefinition.class.getName()); 054 @SuppressWarnings("PMD.FieldNamingConventions") 055 private static final Gson gson = new JSON().getGson(); 056 @SuppressWarnings("PMD.FieldNamingConventions") 057 private static final ObjectMapper objectMapper 058 = new ObjectMapper().registerModule(new JavaTimeModule()); 059 060 private final Model model; 061 private VmExtraData extraData; 062 063 /** 064 * The VM state from the VM definition. 065 */ 066 public enum RequestedVmState { 067 STOPPED, RUNNING 068 } 069 070 /** 071 * Permissions for accessing and manipulating the VM. 072 */ 073 public enum Permission { 074 START("start"), STOP("stop"), RESET("reset"), 075 ACCESS_CONSOLE("accessConsole"), TAKE_CONSOLE("takeConsole"); 076 077 @SuppressWarnings("PMD.UseConcurrentHashMap") 078 private static Map<String, Permission> reprs = new HashMap<>(); 079 080 static { 081 for (var value : EnumSet.allOf(Permission.class)) { 082 reprs.put(value.repr, value); 083 } 084 } 085 086 private final String repr; 087 088 Permission(String repr) { 089 this.repr = repr; 090 } 091 092 /** 093 * Create permission from representation in CRD. 094 * 095 * @param value the value 096 * @return the permission 097 */ 098 @SuppressWarnings("PMD.AvoidLiteralsInIfCondition") 099 public static Set<Permission> parse(String value) { 100 if ("*".equals(value)) { 101 return EnumSet.allOf(Permission.class); 102 } 103 return Set.of(reprs.get(value)); 104 } 105 106 /** 107 * To string. 108 * 109 * @return the string 110 */ 111 @Override 112 public String toString() { 113 return repr; 114 } 115 } 116 117 /** 118 * Permissions granted to a user or role. 119 * 120 * @param user the user 121 * @param role the role 122 * @param may the may 123 */ 124 public record Grant(String user, String role, Set<Permission> may) { 125 126 /** 127 * To string. 128 * 129 * @return the string 130 */ 131 @Override 132 public String toString() { 133 StringBuilder builder = new StringBuilder(); 134 if (user != null) { 135 builder.append("User ").append(user); 136 } else { 137 builder.append("Role ").append(role); 138 } 139 builder.append(" may=").append(may).append(']'); 140 return builder.toString(); 141 } 142 } 143 144 /** 145 * Instantiates a new vm definition. 146 * 147 * @param delegate the delegate 148 * @param json the json 149 */ 150 public VmDefinition(Gson delegate, JsonObject json) { 151 super(delegate, json); 152 model = gson.fromJson(json, Model.class); 153 } 154 155 /** 156 * Gets the spec. 157 * 158 * @return the spec 159 */ 160 public Map<String, Object> spec() { 161 return model.getSpec(); 162 } 163 164 /** 165 * Get a value from the spec using {@link DataPath#get}. 166 * 167 * @param <T> the generic type 168 * @param selectors the selectors 169 * @return the value, if found 170 */ 171 public <T> Optional<T> fromSpec(Object... selectors) { 172 return DataPath.get(spec(), selectors); 173 } 174 175 /** 176 * The pools that this VM belongs to. 177 * 178 * @return the list 179 */ 180 public List<String> pools() { 181 return this.<List<String>> fromSpec("pools") 182 .orElse(Collections.emptyList()); 183 } 184 185 /** 186 * Get a value from the `spec().get("vm")` using {@link DataPath#get}. 187 * 188 * @param <T> the generic type 189 * @param selectors the selectors 190 * @return the value, if found 191 */ 192 public <T> Optional<T> fromVm(Object... selectors) { 193 return DataPath.get(spec(), "vm") 194 .flatMap(vm -> DataPath.get(vm, selectors)); 195 } 196 197 /** 198 * Gets the status. 199 * 200 * @return the status 201 */ 202 public Map<String, Object> status() { 203 return model.getStatus(); 204 } 205 206 /** 207 * Get a value from the status using {@link DataPath#get}. 208 * 209 * @param <T> the generic type 210 * @param selectors the selectors 211 * @return the value, if found 212 */ 213 public <T> Optional<T> fromStatus(Object... selectors) { 214 return DataPath.get(status(), selectors); 215 } 216 217 /** 218 * The pool that the VM was taken from. 219 * 220 * @return the optional 221 */ 222 public Optional<String> assignedFrom() { 223 return fromStatus(Status.ASSIGNMENT, "pool"); 224 } 225 226 /** 227 * The user that the VM was assigned to. 228 * 229 * @return the optional 230 */ 231 public Optional<String> assignedTo() { 232 return fromStatus(Status.ASSIGNMENT, "user"); 233 } 234 235 /** 236 * Last usage of assigned VM. 237 * 238 * @return the optional 239 */ 240 public Optional<Instant> assignmentLastUsed() { 241 return this.<String> fromStatus(Status.ASSIGNMENT, "lastUsed") 242 .map(Instant::parse); 243 } 244 245 /** 246 * Return a condition from the status. 247 * 248 * @param name the condition's name 249 * @return the status, if the condition is defined 250 */ 251 public Optional<V1Condition> condition(String name) { 252 return this.<List<Map<String, Object>>> fromStatus("conditions") 253 .orElse(Collections.emptyList()).stream() 254 .filter(cond -> DataPath.get(cond, "type") 255 .map(name::equals).orElse(false)) 256 .findFirst() 257 .map(cond -> objectMapper.convertValue(cond, V1Condition.class)); 258 } 259 260 /** 261 * Return a condition's status. 262 * 263 * @param name the condition's name 264 * @return the status, if the condition is defined 265 */ 266 public Optional<Boolean> conditionStatus(String name) { 267 return this.<List<Map<String, Object>>> fromStatus("conditions") 268 .orElse(Collections.emptyList()).stream() 269 .filter(cond -> DataPath.get(cond, "type") 270 .map(name::equals).orElse(false)) 271 .findFirst().map(cond -> DataPath.get(cond, "status") 272 .map("True"::equals).orElse(false)); 273 } 274 275 /** 276 * Return true if the console is in use. 277 * 278 * @return true, if successful 279 */ 280 public boolean consoleConnected() { 281 return conditionStatus("ConsoleConnected").orElse(false); 282 } 283 284 /** 285 * Return the last known console user. 286 * 287 * @return the optional 288 */ 289 public Optional<String> consoleUser() { 290 return this.<String> fromStatus(Status.CONSOLE_USER); 291 } 292 293 /** 294 * Set extra data (unknown to kubernetes). 295 * @return the VM definition 296 */ 297 /* default */ VmDefinition extra(VmExtraData extraData) { 298 this.extraData = extraData; 299 return this; 300 } 301 302 /** 303 * Return the extra data. 304 * 305 * @return the data 306 */ 307 public Optional<VmExtraData> extra() { 308 return Optional.ofNullable(extraData); 309 } 310 311 /** 312 * Returns the definition's name. 313 * 314 * @return the string 315 */ 316 public String name() { 317 return metadata().getName(); 318 } 319 320 /** 321 * Returns the definition's namespace. 322 * 323 * @return the string 324 */ 325 public String namespace() { 326 return metadata().getNamespace(); 327 } 328 329 /** 330 * Return the requested VM state. 331 * 332 * @return the string 333 */ 334 public RequestedVmState vmState() { 335 return fromVm("state") 336 .map(s -> "Running".equals(s) ? RequestedVmState.RUNNING 337 : RequestedVmState.STOPPED) 338 .orElse(RequestedVmState.STOPPED); 339 } 340 341 /** 342 * Collect all permissions for the given user with the given roles. 343 * If permission "takeConsole" is granted, the result will also 344 * contain "accessConsole" to simplify checks. 345 * 346 * @param user the user 347 * @param roles the roles 348 * @return the sets the 349 */ 350 public Set<Permission> permissionsFor(String user, 351 Collection<String> roles) { 352 var result = this.<List<Map<String, Object>>> fromSpec("permissions") 353 .orElse(Collections.emptyList()).stream() 354 .filter(p -> DataPath.get(p, "user").map(u -> u.equals(user)) 355 .orElse(false) 356 || DataPath.get(p, "role").map(roles::contains).orElse(false)) 357 .map(p -> DataPath.<List<String>> get(p, "may") 358 .orElse(Collections.emptyList()).stream()) 359 .flatMap(Function.identity()) 360 .map(Permission::parse).map(Set::stream) 361 .flatMap(Function.identity()) 362 .collect(Collectors.toCollection(HashSet::new)); 363 364 // Take console implies access console, simplify checks 365 if (result.contains(Permission.TAKE_CONSOLE)) { 366 result.add(Permission.ACCESS_CONSOLE); 367 } 368 return result; 369 } 370 371 /** 372 * Check if the console is accessible. Returns true if the console is 373 * currently unused, used by the given user or if the permissions 374 * allow taking over the console. 375 * 376 * @param user the user 377 * @param permissions the permissions 378 * @return true, if successful 379 */ 380 public boolean consoleAccessible(String user, Set<Permission> permissions) { 381 return !conditionStatus("ConsoleConnected").orElse(true) 382 || consoleUser().map(cu -> cu.equals(user)).orElse(true) 383 || permissions.contains(VmDefinition.Permission.TAKE_CONSOLE); 384 } 385 386 /** 387 * Get the display password serial. 388 * 389 * @return the optional 390 */ 391 public Optional<Long> displayPasswordSerial() { 392 return this.<Number> fromStatus(Status.DISPLAY_PASSWORD_SERIAL) 393 .map(Number::longValue); 394 } 395 396 /** 397 * Hash code. 398 * 399 * @return the int 400 */ 401 @Override 402 public int hashCode() { 403 return Objects.hash(metadata().getNamespace(), metadata().getName()); 404 } 405 406 /** 407 * Equals. 408 * 409 * @param obj the obj 410 * @return true, if successful 411 */ 412 @Override 413 public boolean equals(Object obj) { 414 if (this == obj) { 415 return true; 416 } 417 if (obj == null) { 418 return false; 419 } 420 if (getClass() != obj.getClass()) { 421 return false; 422 } 423 VmDefinition other = (VmDefinition) obj; 424 return Objects.equals(metadata().getNamespace(), 425 other.metadata().getNamespace()) 426 && Objects.equals(metadata().getName(), other.metadata().getName()); 427 } 428 429 /** 430 * The Class Model. 431 */ 432 public static class Model { 433 434 private Map<String, Object> spec; 435 private Map<String, Object> status; 436 437 /** 438 * Gets the spec. 439 * 440 * @return the spec 441 */ 442 public Map<String, Object> getSpec() { 443 return spec; 444 } 445 446 /** 447 * Sets the spec. 448 * 449 * @param spec the spec to set 450 */ 451 public void setSpec(Map<String, Object> spec) { 452 this.spec = spec; 453 } 454 455 /** 456 * Gets the status. 457 * 458 * @return the status 459 */ 460 public Map<String, Object> getStatus() { 461 return status; 462 } 463 464 /** 465 * Sets the status. 466 * 467 * @param status the status to set 468 */ 469 public void setStatus(Map<String, Object> status) { 470 this.status = status; 471 } 472 473 } 474 475}