Skip to content

Commit

Permalink
Merge branch 'feature/console-action' into 'main'
Browse files Browse the repository at this point in the history
Add more actions to VM management conlet.

See merge request org/jdrupes/vm-operator!12
  • Loading branch information
mnlipp committed Jan 30, 2025
2 parents 85be5b9 + ebda413 commit 4956658
Show file tree
Hide file tree
Showing 16 changed files with 710 additions and 146 deletions.
1 change: 1 addition & 0 deletions dev-example/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
admin:
- "*"
operator:
- org.jdrupes.vmoperator.vmmgmt.VmMgmt
- org.jdrupes.vmoperator.vmaccess.VmAccess
user:
- org.jdrupes.vmoperator.vmaccess.VmAccess
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* VM-Operator
* Copyright (C) 2024 Michael N. Lipp
* Copyright (C) 2025 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
Expand All @@ -22,18 +22,24 @@
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import io.kubernetes.client.openapi.models.V1Condition;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.util.Strings;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.time.Instant;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import org.jdrupes.vmoperator.util.DataPath;

Expand All @@ -43,7 +49,11 @@
@SuppressWarnings({ "PMD.DataClass", "PMD.TooManyMethods" })
public class VmDefinition {

private static ObjectMapper objectMapper
@SuppressWarnings("PMD.FieldNamingConventions")
private static final Logger logger
= Logger.getLogger(VmDefinition.class.getName());
@SuppressWarnings("PMD.FieldNamingConventions")
private static final ObjectMapper objectMapper
= new ObjectMapper().registerModule(new JavaTimeModule());

private String kind;
Expand Down Expand Up @@ -427,14 +437,16 @@ public RequestedVmState vmState() {

/**
* Collect all permissions for the given user with the given roles.
* If permission "takeConsole" is granted, the result will also
* contain "accessConsole" to simplify checks.
*
* @param user the user
* @param roles the roles
* @return the sets the
*/
public Set<Permission> permissionsFor(String user,
Collection<String> roles) {
return this.<List<Map<String, Object>>> fromSpec("permissions")
var result = this.<List<Map<String, Object>>> fromSpec("permissions")
.orElse(Collections.emptyList()).stream()
.filter(p -> DataPath.get(p, "user").map(u -> u.equals(user))
.orElse(false)
Expand All @@ -443,7 +455,29 @@ public Set<Permission> permissionsFor(String user,
.orElse(Collections.emptyList()).stream())
.flatMap(Function.identity())
.map(Permission::parse).map(Set::stream)
.flatMap(Function.identity()).collect(Collectors.toSet());
.flatMap(Function.identity())
.collect(Collectors.toCollection(HashSet::new));

// Take console implies access console, simplify checks
if (result.contains(Permission.TAKE_CONSOLE)) {
result.add(Permission.ACCESS_CONSOLE);
}
return result;
}

/**
* Check if the console is accessible. Returns true if the console is
* currently unused, used by the given user or if the permissions
* allow taking over the console.
*
* @param user the user
* @param permissions the permissions
* @return true, if successful
*/
public boolean consoleAccessible(String user, Set<Permission> permissions) {
return !conditionStatus("ConsoleConnected").orElse(true)
|| consoleUser().map(cu -> cu.equals(user)).orElse(true)
|| permissions.contains(VmDefinition.Permission.TAKE_CONSOLE);
}

/**
Expand All @@ -456,6 +490,78 @@ public Optional<Long> displayPasswordSerial() {
.map(Number::longValue);
}

/**
* Create a connection file.
*
* @param password the password
* @param preferredIpVersion the preferred IP version
* @param deleteConnectionFile the delete connection file
* @return the string
*/
public String connectionFile(String password,
Class<?> preferredIpVersion, boolean deleteConnectionFile) {
var addr = displayIp(preferredIpVersion);
if (addr.isEmpty()) {
logger.severe(() -> "Failed to find display IP for " + name());
return null;
}
var port = this.<Number> fromVm("display", "spice", "port")
.map(Number::longValue);
if (port.isEmpty()) {
logger.severe(() -> "No port defined for display of " + name());
return null;
}
StringBuffer data = new StringBuffer(100)
.append("[virt-viewer]\ntype=spice\nhost=")
.append(addr.get().getHostAddress()).append("\nport=")
.append(port.get().toString())
.append('\n');
if (password != null) {
data.append("password=").append(password).append('\n');
}
this.<String> fromVm("display", "spice", "proxyUrl")
.ifPresent(u -> {
if (!Strings.isNullOrEmpty(u)) {
data.append("proxy=").append(u).append('\n');
}
});
if (deleteConnectionFile) {
data.append("delete-this-file=1\n");
}
return data.toString();
}

private Optional<InetAddress> displayIp(Class<?> preferredIpVersion) {
Optional<String> server = fromVm("display", "spice", "server");
if (server.isPresent()) {
var srv = server.get();
try {
var addr = InetAddress.getByName(srv);
logger.fine(() -> "Using IP address from CRD for "
+ getMetadata().getName() + ": " + addr);
return Optional.of(addr);
} catch (UnknownHostException e) {
logger.log(Level.SEVERE, e, () -> "Invalid server address "
+ srv + ": " + e.getMessage());
return Optional.empty();
}
}
var addrs = Optional.<List<String>> ofNullable(
extra("nodeAddresses")).orElse(Collections.emptyList()).stream()
.map(a -> {
try {
return InetAddress.getByName(a);
} catch (UnknownHostException e) {
logger.warning(() -> "Invalid IP address: " + a);
return null;
}
}).filter(a -> a != null).toList();
logger.fine(() -> "Known IP addresses for " + name() + ": " + addrs);
return addrs.stream()
.filter(a -> preferredIpVersion.isAssignableFrom(a.getClass()))
.findFirst().or(() -> addrs.stream().findFirst());
}

/**
* Hash code.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ public ChannelManager() {
this(k -> null);
}

/**
* Return all keys.
*
* @return the keys.
*/
@Override
public Set<K> keys() {
return entries.keySet();
Expand Down Expand Up @@ -113,6 +118,18 @@ public ChannelManager<K, C, A> put(K key, C channel) {
return this;
}

/**
* Creates a new channel without adding it to the channel manager.
* After fully initializing the channel, it should be added to the
* manager using {@link #put(K, C)}.
*
* @param key the key
* @return the c
*/
public C createChannel(K key) {
return supplier.apply(key);
}

/**
* Returns the {@link Channel} for the given name, creating it using
* the supplier passed to the constructor if it doesn't exist yet.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.util.Comparator;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
Expand Down Expand Up @@ -125,7 +126,12 @@ private void purge() throws ApiException {
protected void handleChange(K8sClient client,
Watch.Response<VmDefinitionModel> response) {
V1ObjectMeta metadata = response.object.getMetadata();
VmChannel channel = channelManager.channelGet(metadata.getName());
AtomicBoolean toBeAdded = new AtomicBoolean(false);
VmChannel channel = channelManager.channel(metadata.getName())
.orElseGet(() -> {
toBeAdded.set(true);
return channelManager.createChannel(metadata.getName());
});

// Get full definition and associate with channel as backup
var vmModel = response.object;
Expand All @@ -151,6 +157,9 @@ protected void handleChange(K8sClient client,
+ response.object.getMetadata());
return;
}
if (toBeAdded.get()) {
channelManager.put(vmDef.name(), channel);
}

// Create and fire changed event. Remove channel from channel
// manager on completion.
Expand Down
Loading

0 comments on commit 4956658

Please sign in to comment.