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