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