Add viewer conlet (#25)
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
This commit is contained in:
parent
b6f0299932
commit
a6525a2289
77 changed files with 2642 additions and 250 deletions
|
|
@ -5,7 +5,7 @@ connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
|
||||||
connection.project.dir=
|
connection.project.dir=
|
||||||
eclipse.preferences.version=1
|
eclipse.preferences.version=1
|
||||||
gradle.user.home=
|
gradle.user.home=
|
||||||
java.home=/usr/lib/jvm/java-17-openjdk-17.0.8.0.7-1.fc37.x86_64
|
java.home=
|
||||||
jvm.arguments=
|
jvm.arguments=
|
||||||
offline.mode=false
|
offline.mode=false
|
||||||
override.workspace.settings=true
|
override.workspace.settings=true
|
||||||
|
|
|
||||||
|
|
@ -1385,9 +1385,20 @@ spec:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
port:
|
port:
|
||||||
|
description: >-
|
||||||
|
Port number used for the Spice server.
|
||||||
type: integer
|
type: integer
|
||||||
default: 5900
|
default: 5900
|
||||||
ticket:
|
server:
|
||||||
|
description: >-
|
||||||
|
Server (address) to use for connecting
|
||||||
|
to the spice server. Defaults to the address
|
||||||
|
of the node that the VM is running on.
|
||||||
|
type: string
|
||||||
|
proxyUrl:
|
||||||
|
description: >-
|
||||||
|
If specified, is copied to the generated
|
||||||
|
viewer configuration files.
|
||||||
type: string
|
type: string
|
||||||
streamingVideo:
|
streamingVideo:
|
||||||
type: string
|
type: string
|
||||||
|
|
@ -1413,7 +1424,8 @@ spec:
|
||||||
default: "0"
|
default: "0"
|
||||||
displayPasswordSerial:
|
displayPasswordSerial:
|
||||||
description: >-
|
description: >-
|
||||||
Counts changes of the display password.
|
Counts changes of the display password. Set to -1
|
||||||
|
by the runner if password protection is not enabled.
|
||||||
type: integer
|
type: integer
|
||||||
default: 0
|
default: 0
|
||||||
conditions:
|
conditions:
|
||||||
|
|
|
||||||
|
|
@ -41,3 +41,7 @@
|
||||||
other:
|
other:
|
||||||
- --org.jdrupes.vmoperator.vmconlet.VmConlet
|
- --org.jdrupes.vmoperator.vmconlet.VmConlet
|
||||||
- org.jgrapes.webconlet.oidclogin.LoginConlet
|
- org.jgrapes.webconlet.oidclogin.LoginConlet
|
||||||
|
"/ComponentCollector":
|
||||||
|
"/VmViewer":
|
||||||
|
displayResource:
|
||||||
|
preferredIpVersion: ipv4
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,10 @@ patches:
|
||||||
# Others cannot use any conlet (except login conlet to log out)
|
# Others cannot use any conlet (except login conlet to log out)
|
||||||
other:
|
other:
|
||||||
- org.jgrapes.webconlet.locallogin.LoginConlet
|
- org.jgrapes.webconlet.locallogin.LoginConlet
|
||||||
|
"/ComponentCollector":
|
||||||
|
"/VmViewer":
|
||||||
|
displayResource:
|
||||||
|
preferredIpVersion: ipv4
|
||||||
- target:
|
- target:
|
||||||
group: apps
|
group: apps
|
||||||
version: v1
|
version: v1
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,4 @@ metadata:
|
||||||
type: Opaque
|
type: Opaque
|
||||||
data:
|
data:
|
||||||
display-password: dGVzdC12bQ==
|
display-password: dGVzdC12bQ==
|
||||||
|
password-expiry: KzMw
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ package org.jdrupes.vmoperator.common;
|
||||||
/**
|
/**
|
||||||
* Some constants.
|
* Some constants.
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("PMD.DataClass")
|
||||||
public class Constants {
|
public class Constants {
|
||||||
|
|
||||||
/** The Constant APP_NAME. */
|
/** The Constant APP_NAME. */
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,6 @@ import org.yaml.snakeyaml.LoaderOptions;
|
||||||
import org.yaml.snakeyaml.Yaml;
|
import org.yaml.snakeyaml.Yaml;
|
||||||
import org.yaml.snakeyaml.constructor.SafeConstructor;
|
import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
|
|
||||||
// TODO: Auto-generated Javadoc
|
|
||||||
/**
|
/**
|
||||||
* Helpers for K8s API.
|
* Helpers for K8s API.
|
||||||
*/
|
*/
|
||||||
|
|
@ -168,6 +167,7 @@ public class K8s {
|
||||||
* @return the object
|
* @return the object
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
|
@SuppressWarnings("PMD.GenericsNaming")
|
||||||
public static <T extends KubernetesObject, LT extends KubernetesListObject>
|
public static <T extends KubernetesObject, LT extends KubernetesListObject>
|
||||||
Optional<T>
|
Optional<T>
|
||||||
get(GenericKubernetesApi<T, LT> api, V1ObjectMeta meta) {
|
get(GenericKubernetesApi<T, LT> api, V1ObjectMeta meta) {
|
||||||
|
|
@ -189,6 +189,7 @@ public class K8s {
|
||||||
* @return the t
|
* @return the t
|
||||||
* @throws ApiException the api exception
|
* @throws ApiException the api exception
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("PMD.GenericsNaming")
|
||||||
public static <T extends KubernetesObject, LT extends KubernetesListObject>
|
public static <T extends KubernetesObject, LT extends KubernetesListObject>
|
||||||
T apply(GenericKubernetesApi<T, LT> api, T existing, String update)
|
T apply(GenericKubernetesApi<T, LT> api, T existing, String update)
|
||||||
throws ApiException {
|
throws ApiException {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,401 @@
|
||||||
|
/*
|
||||||
|
* VM-Operator
|
||||||
|
* Copyright (C) 2024 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
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jdrupes.vmoperator.common;
|
||||||
|
|
||||||
|
import io.kubernetes.client.Discovery.APIResource;
|
||||||
|
import io.kubernetes.client.apimachinery.GroupVersionKind;
|
||||||
|
import io.kubernetes.client.common.KubernetesListObject;
|
||||||
|
import io.kubernetes.client.common.KubernetesObject;
|
||||||
|
import io.kubernetes.client.custom.V1Patch;
|
||||||
|
import io.kubernetes.client.openapi.ApiException;
|
||||||
|
import io.kubernetes.client.util.Strings;
|
||||||
|
import io.kubernetes.client.util.generic.GenericKubernetesApi;
|
||||||
|
import io.kubernetes.client.util.generic.options.GetOptions;
|
||||||
|
import io.kubernetes.client.util.generic.options.ListOptions;
|
||||||
|
import io.kubernetes.client.util.generic.options.PatchOptions;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A stub for cluster scoped objects. This stub provides the
|
||||||
|
* functions common to all Kubernetes objects, but uses variables
|
||||||
|
* for all types. This class should be used as base class only.
|
||||||
|
*
|
||||||
|
* @param <O> the generic type
|
||||||
|
* @param <L> the generic type
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
||||||
|
public class K8sClusterGenericStub<O extends KubernetesObject,
|
||||||
|
L extends KubernetesListObject> {
|
||||||
|
protected final K8sClient client;
|
||||||
|
private final GenericKubernetesApi<O, L> api;
|
||||||
|
protected final APIResource context;
|
||||||
|
protected final String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new stub for the object specified. If the object
|
||||||
|
* exists in the context specified, the version (see
|
||||||
|
* {@link #version()} is bound to the existing object's version.
|
||||||
|
* Else the stub is dangling with the version set to the context's
|
||||||
|
* preferred version.
|
||||||
|
*
|
||||||
|
* @param objectClass the object class
|
||||||
|
* @param objectListClass the object list class
|
||||||
|
* @param client the client
|
||||||
|
* @param context the context
|
||||||
|
* @param name the name
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
|
||||||
|
protected K8sClusterGenericStub(Class<O> objectClass,
|
||||||
|
Class<L> objectListClass, K8sClient client, APIResource context,
|
||||||
|
String name) {
|
||||||
|
this.client = client;
|
||||||
|
this.name = name;
|
||||||
|
|
||||||
|
// Bind version
|
||||||
|
var foundVersion = context.getPreferredVersion();
|
||||||
|
GenericKubernetesApi<O, L> testApi = null;
|
||||||
|
GetOptions mdOpts
|
||||||
|
= new GetOptions().isPartialObjectMetadataRequest(true);
|
||||||
|
for (var version : candidateVersions(context)) {
|
||||||
|
testApi = new GenericKubernetesApi<>(objectClass, objectListClass,
|
||||||
|
context.getGroup(), version, context.getResourcePlural(),
|
||||||
|
client);
|
||||||
|
if (testApi.get(name, mdOpts).isSuccess()) {
|
||||||
|
foundVersion = version;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (foundVersion.equals(context.getPreferredVersion())) {
|
||||||
|
this.context = context;
|
||||||
|
} else {
|
||||||
|
this.context = K8s.preferred(context, foundVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
api = Optional.ofNullable(testApi)
|
||||||
|
.orElseGet(() -> new GenericKubernetesApi<>(objectClass,
|
||||||
|
objectListClass, group(), version(), plural(), client));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the context.
|
||||||
|
*
|
||||||
|
* @return the context
|
||||||
|
*/
|
||||||
|
public APIResource context() {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the group.
|
||||||
|
*
|
||||||
|
* @return the group
|
||||||
|
*/
|
||||||
|
public String group() {
|
||||||
|
return context.getGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the version.
|
||||||
|
*
|
||||||
|
* @return the version
|
||||||
|
*/
|
||||||
|
public String version() {
|
||||||
|
return context.getPreferredVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the kind.
|
||||||
|
*
|
||||||
|
* @return the kind
|
||||||
|
*/
|
||||||
|
public String kind() {
|
||||||
|
return context.getKind();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the plural.
|
||||||
|
*
|
||||||
|
* @return the plural
|
||||||
|
*/
|
||||||
|
public String plural() {
|
||||||
|
return context.getResourcePlural();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the name.
|
||||||
|
*
|
||||||
|
* @return the name
|
||||||
|
*/
|
||||||
|
public String name() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the Kubernetes object.
|
||||||
|
*
|
||||||
|
* @throws ApiException the API exception
|
||||||
|
*/
|
||||||
|
public void delete() throws ApiException {
|
||||||
|
var result = api.delete(name);
|
||||||
|
if (result.isSuccess()
|
||||||
|
|| result.getHttpStatusCode() == HttpURLConnection.HTTP_NOT_FOUND) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result.throwsApiException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves and returns the current state of the object.
|
||||||
|
*
|
||||||
|
* @return the object's state
|
||||||
|
* @throws ApiException the api exception
|
||||||
|
*/
|
||||||
|
public Optional<O> model() throws ApiException {
|
||||||
|
return K8s.optional(api.get(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the object's status.
|
||||||
|
*
|
||||||
|
* @param object the current state of the object (passed to `status`)
|
||||||
|
* @param status function that returns the new status
|
||||||
|
* @return the updated model or empty if not successful
|
||||||
|
* @throws ApiException the api exception
|
||||||
|
*/
|
||||||
|
public Optional<O> updateStatus(O object,
|
||||||
|
Function<O, Object> status) throws ApiException {
|
||||||
|
return K8s.optional(api.updateStatus(object, status));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the status.
|
||||||
|
*
|
||||||
|
* @param status the status
|
||||||
|
* @return the kubernetes api response
|
||||||
|
* the updated model or empty if not successful
|
||||||
|
* @throws ApiException the api exception
|
||||||
|
*/
|
||||||
|
public Optional<O> updateStatus(Function<O, Object> status)
|
||||||
|
throws ApiException {
|
||||||
|
return updateStatus(api.get(name).throwsApiException().getObject(),
|
||||||
|
status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patch the object.
|
||||||
|
*
|
||||||
|
* @param patchType the patch type
|
||||||
|
* @param patch the patch
|
||||||
|
* @param options the options
|
||||||
|
* @return the kubernetes api response
|
||||||
|
* @throws ApiException the api exception
|
||||||
|
*/
|
||||||
|
public Optional<O> patch(String patchType, V1Patch patch,
|
||||||
|
PatchOptions options) throws ApiException {
|
||||||
|
return K8s
|
||||||
|
.optional(api.patch(name, patchType, patch, options));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patch the object using default options.
|
||||||
|
*
|
||||||
|
* @param patchType the patch type
|
||||||
|
* @param patch the patch
|
||||||
|
* @return the kubernetes api response
|
||||||
|
* @throws ApiException the api exception
|
||||||
|
*/
|
||||||
|
public Optional<O>
|
||||||
|
patch(String patchType, V1Patch patch) throws ApiException {
|
||||||
|
PatchOptions opts = new PatchOptions();
|
||||||
|
return patch(patchType, patch, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A supplier for generic stubs.
|
||||||
|
*
|
||||||
|
* @param <O> the object type
|
||||||
|
* @param <L> the object list type
|
||||||
|
* @param <R> the result type
|
||||||
|
*/
|
||||||
|
public interface GenericSupplier<O extends KubernetesObject,
|
||||||
|
L extends KubernetesListObject,
|
||||||
|
R extends K8sClusterGenericStub<O, L>> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a new stub.
|
||||||
|
*
|
||||||
|
* @param objectClass the object class
|
||||||
|
* @param objectListClass the object list class
|
||||||
|
* @param client the client
|
||||||
|
* @param context the API resource
|
||||||
|
* @param name the name
|
||||||
|
* @return the result
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("PMD.UseObjectForClearerAPI")
|
||||||
|
R get(Class<O> objectClass, Class<L> objectListClass, K8sClient client,
|
||||||
|
APIResource context, String name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("PMD.UseLocaleWithCaseConversions")
|
||||||
|
public String toString() {
|
||||||
|
return (Strings.isNullOrEmpty(group()) ? "" : group() + "/")
|
||||||
|
+ version().toUpperCase() + kind() + " " + name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an object stub. If the version in parameter
|
||||||
|
* `gvk` is an empty string, the stub refers to the first object
|
||||||
|
* found with matching group and kind.
|
||||||
|
*
|
||||||
|
* @param <O> the object type
|
||||||
|
* @param <L> the object list type
|
||||||
|
* @param <R> the stub type
|
||||||
|
* @param objectClass the object class
|
||||||
|
* @param objectListClass the object list class
|
||||||
|
* @param client the client
|
||||||
|
* @param gvk the group, version and kind
|
||||||
|
* @param name the name
|
||||||
|
* @param provider the provider
|
||||||
|
* @return the stub if the object exists
|
||||||
|
* @throws ApiException the api exception
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" })
|
||||||
|
public static <O extends KubernetesObject, L extends KubernetesListObject,
|
||||||
|
R extends K8sClusterGenericStub<O, L>>
|
||||||
|
R get(Class<O> objectClass, Class<L> objectListClass,
|
||||||
|
K8sClient client, GroupVersionKind gvk, String name,
|
||||||
|
GenericSupplier<O, L, R> provider) throws ApiException {
|
||||||
|
var context = K8s.context(client, gvk.getGroup(), gvk.getVersion(),
|
||||||
|
gvk.getKind());
|
||||||
|
if (context.isEmpty()) {
|
||||||
|
throw new ApiException("No known API for " + gvk.getGroup()
|
||||||
|
+ "/" + gvk.getVersion() + " " + gvk.getKind());
|
||||||
|
}
|
||||||
|
return provider.get(objectClass, objectListClass, client, context.get(),
|
||||||
|
name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an object stub.
|
||||||
|
*
|
||||||
|
* @param <O> the object type
|
||||||
|
* @param <L> the object list type
|
||||||
|
* @param <R> the stub type
|
||||||
|
* @param objectClass the object class
|
||||||
|
* @param objectListClass the object list class
|
||||||
|
* @param client the client
|
||||||
|
* @param context the context
|
||||||
|
* @param name the name
|
||||||
|
* @param provider the provider
|
||||||
|
* @return the stub if the object exists
|
||||||
|
* @throws ApiException the api exception
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
|
||||||
|
"PMD.UseObjectForClearerAPI" })
|
||||||
|
public static <O extends KubernetesObject, L extends KubernetesListObject,
|
||||||
|
R extends K8sClusterGenericStub<O, L>>
|
||||||
|
R get(Class<O> objectClass, Class<L> objectListClass,
|
||||||
|
K8sClient client, APIResource context, String name,
|
||||||
|
GenericSupplier<O, L, R> provider) {
|
||||||
|
return provider.get(objectClass, objectListClass, client, context,
|
||||||
|
name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an object stub for a newly created object.
|
||||||
|
*
|
||||||
|
* @param <O> the object type
|
||||||
|
* @param <L> the object list type
|
||||||
|
* @param <R> the stub type
|
||||||
|
* @param objectClass the object class
|
||||||
|
* @param objectListClass the object list class
|
||||||
|
* @param client the client
|
||||||
|
* @param context the context
|
||||||
|
* @param model the model
|
||||||
|
* @param provider the provider
|
||||||
|
* @return the stub if the object exists
|
||||||
|
* @throws ApiException the api exception
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
|
||||||
|
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
|
||||||
|
public static <O extends KubernetesObject, L extends KubernetesListObject,
|
||||||
|
R extends K8sClusterGenericStub<O, L>>
|
||||||
|
R create(Class<O> objectClass, Class<L> objectListClass,
|
||||||
|
K8sClient client, APIResource context, O model,
|
||||||
|
GenericSupplier<O, L, R> provider) throws ApiException {
|
||||||
|
var api = new GenericKubernetesApi<>(objectClass, objectListClass,
|
||||||
|
context.getGroup(), context.getPreferredVersion(),
|
||||||
|
context.getResourcePlural(), client);
|
||||||
|
api.create(model).throwsApiException();
|
||||||
|
return provider.get(objectClass, objectListClass, client,
|
||||||
|
context, model.getMetadata().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the stubs for the objects that match
|
||||||
|
* the criteria from the given options.
|
||||||
|
*
|
||||||
|
* @param <O> the object type
|
||||||
|
* @param <L> the object list type
|
||||||
|
* @param <R> the stub type
|
||||||
|
* @param objectClass the object class
|
||||||
|
* @param objectListClass the object list class
|
||||||
|
* @param client the client
|
||||||
|
* @param context the context
|
||||||
|
* @param options the options
|
||||||
|
* @param provider the provider
|
||||||
|
* @return the collection
|
||||||
|
* @throws ApiException the api exception
|
||||||
|
*/
|
||||||
|
public static <O extends KubernetesObject, L extends KubernetesListObject,
|
||||||
|
R extends K8sClusterGenericStub<O, L>>
|
||||||
|
Collection<R> list(Class<O> objectClass, Class<L> objectListClass,
|
||||||
|
K8sClient client, APIResource context,
|
||||||
|
ListOptions options, GenericSupplier<O, L, R> provider)
|
||||||
|
throws ApiException {
|
||||||
|
var result = new ArrayList<R>();
|
||||||
|
for (var version : candidateVersions(context)) {
|
||||||
|
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
|
||||||
|
var api = new GenericKubernetesApi<>(objectClass, objectListClass,
|
||||||
|
context.getGroup(), version, context.getResourcePlural(),
|
||||||
|
client);
|
||||||
|
var objs = api.list(options).throwsApiException();
|
||||||
|
for (var item : objs.getObject().getItems()) {
|
||||||
|
result.add(provider.get(objectClass, objectListClass, client,
|
||||||
|
context, item.getMetadata().getName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> candidateVersions(APIResource context) {
|
||||||
|
var result = new LinkedList<>(context.getVersions());
|
||||||
|
result.remove(context.getPreferredVersion());
|
||||||
|
result.add(0, context.getPreferredVersion());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -44,12 +44,13 @@ public class K8sDynamicModelTypeAdapterFactory implements TypeAdapterFactory {
|
||||||
* this factory
|
* this factory
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
|
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
|
||||||
if (TypeToken.get(K8sDynamicModel.class).equals(typeToken)) {
|
if (TypeToken.get(K8sDynamicModel.class).equals(typeToken)) {
|
||||||
return (TypeAdapter<T>) (new K8sDynamicModelCreator(gson));
|
return (TypeAdapter<T>) new K8sDynamicModelCreator(gson);
|
||||||
}
|
}
|
||||||
if (TypeToken.get(K8sDynamicModels.class).equals(typeToken)) {
|
if (TypeToken.get(K8sDynamicModels.class).equals(typeToken)) {
|
||||||
return (TypeAdapter<T>) (new K8sDynamicModelsCreator(gson));
|
return (TypeAdapter<T>) new K8sDynamicModelsCreator(gson);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ public class K8sObserver<O extends KubernetesObject,
|
||||||
context.getResourcePlural(), client);
|
context.getResourcePlural(), client);
|
||||||
thread = new Thread(() -> {
|
thread = new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
logger.info(() -> "Watching " + context.getResourcePlural()
|
logger.config(() -> "Watching " + context.getResourcePlural()
|
||||||
+ " (" + context.getPreferredVersion() + ")"
|
+ " (" + context.getPreferredVersion() + ")"
|
||||||
+ " in " + namespace);
|
+ " in " + namespace);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
* VM-Operator
|
||||||
|
* Copyright (C) 2024 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
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jdrupes.vmoperator.common;
|
||||||
|
|
||||||
|
import io.kubernetes.client.Discovery.APIResource;
|
||||||
|
import io.kubernetes.client.openapi.ApiException;
|
||||||
|
import io.kubernetes.client.openapi.models.V1Node;
|
||||||
|
import io.kubernetes.client.openapi.models.V1NodeList;
|
||||||
|
import io.kubernetes.client.util.generic.options.ListOptions;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A stub for nodes (v1).
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
||||||
|
public class K8sV1NodeStub extends K8sClusterGenericStub<V1Node, V1NodeList> {
|
||||||
|
|
||||||
|
public static final APIResource CONTEXT = new APIResource("", List.of("v1"),
|
||||||
|
"v1", "Node", true, "nodes", "node");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new stub.
|
||||||
|
*
|
||||||
|
* @param client the client
|
||||||
|
* @param name the name
|
||||||
|
*/
|
||||||
|
protected K8sV1NodeStub(K8sClient client, String name) {
|
||||||
|
super(V1Node.class, V1NodeList.class, client, CONTEXT, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the stub for the given name.
|
||||||
|
*
|
||||||
|
* @param client the client
|
||||||
|
* @param name the name
|
||||||
|
* @return the config map stub
|
||||||
|
*/
|
||||||
|
public static K8sV1NodeStub get(K8sClient client, String name) {
|
||||||
|
return new K8sV1NodeStub(client, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the stubs for the objects that match
|
||||||
|
* the criteria from the given options.
|
||||||
|
*
|
||||||
|
* @param client the client
|
||||||
|
* @param options the options
|
||||||
|
* @return the collection
|
||||||
|
* @throws ApiException the api exception
|
||||||
|
*/
|
||||||
|
public static Collection<K8sV1NodeStub> list(K8sClient client,
|
||||||
|
ListOptions options) throws ApiException {
|
||||||
|
return K8sClusterGenericStub.list(V1Node.class, V1NodeList.class,
|
||||||
|
client, CONTEXT, options, K8sV1NodeStub::getGeneric);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide {@link GenericSupplier}.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({ "PMD.UnusedFormalParameter",
|
||||||
|
"PMD.UnusedPrivateMethod" })
|
||||||
|
private static K8sV1NodeStub getGeneric(Class<V1Node> objectClass,
|
||||||
|
Class<V1NodeList> objectListClass, K8sClient client,
|
||||||
|
APIResource context, String name) {
|
||||||
|
return new K8sV1NodeStub(client, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -79,7 +79,8 @@ public class K8sV1PodStub extends K8sGenericStub<V1Pod, V1PodList> {
|
||||||
/**
|
/**
|
||||||
* Provide {@link GenericSupplier}.
|
* Provide {@link GenericSupplier}.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("PMD.UnusedFormalParameter")
|
@SuppressWarnings({ "PMD.UnusedFormalParameter",
|
||||||
|
"PMD.UnusedPrivateMethod" })
|
||||||
private static K8sV1PodStub getGeneric(Class<V1Pod> objectClass,
|
private static K8sV1PodStub getGeneric(Class<V1Pod> objectClass,
|
||||||
Class<V1PodList> objectListClass, K8sClient client,
|
Class<V1PodList> objectListClass, K8sClient client,
|
||||||
APIResource context, String namespace, String name) {
|
APIResource context, String namespace, String name) {
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,8 @@ public class K8sV1SecretStub extends K8sGenericStub<V1Secret, V1SecretList> {
|
||||||
/**
|
/**
|
||||||
* Provide {@link GenericSupplier}.
|
* Provide {@link GenericSupplier}.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("PMD.UnusedFormalParameter")
|
@SuppressWarnings({ "PMD.UnusedFormalParameter",
|
||||||
|
"PMD.UnusedPrivateMethod" })
|
||||||
private static K8sV1SecretStub getGeneric(Class<V1Secret> objectClass,
|
private static K8sV1SecretStub getGeneric(Class<V1Secret> objectClass,
|
||||||
Class<V1SecretList> objectListClass, K8sClient client,
|
Class<V1SecretList> objectListClass, K8sClient client,
|
||||||
APIResource context, String namespace, String name) {
|
APIResource context, String namespace, String name) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
/*
|
||||||
|
* VM-Operator
|
||||||
|
* Copyright (C) 2024 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
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jdrupes.vmoperator.common;
|
||||||
|
|
||||||
|
import io.kubernetes.client.Discovery.APIResource;
|
||||||
|
import io.kubernetes.client.openapi.ApiException;
|
||||||
|
import io.kubernetes.client.openapi.models.V1Service;
|
||||||
|
import io.kubernetes.client.openapi.models.V1ServiceList;
|
||||||
|
import io.kubernetes.client.util.generic.options.ListOptions;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import org.jdrupes.vmoperator.common.K8sGenericStub.GenericSupplier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A stub for secrets (v1).
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
||||||
|
public class K8sV1ServiceStub extends K8sGenericStub<V1Service, V1ServiceList> {
|
||||||
|
|
||||||
|
public static final APIResource CONTEXT = new APIResource("", List.of("v1"),
|
||||||
|
"v1", "Service", true, "services", "service");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new stub.
|
||||||
|
*
|
||||||
|
* @param client the client
|
||||||
|
* @param namespace the namespace
|
||||||
|
* @param name the name
|
||||||
|
*/
|
||||||
|
protected K8sV1ServiceStub(K8sClient client, String namespace,
|
||||||
|
String name) {
|
||||||
|
super(V1Service.class, V1ServiceList.class, client, CONTEXT, namespace,
|
||||||
|
name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the stub for the given namespace and name.
|
||||||
|
*
|
||||||
|
* @param client the client
|
||||||
|
* @param namespace the namespace
|
||||||
|
* @param name the name
|
||||||
|
* @return the config map stub
|
||||||
|
*/
|
||||||
|
public static K8sV1ServiceStub get(K8sClient client, String namespace,
|
||||||
|
String name) {
|
||||||
|
return new K8sV1ServiceStub(client, namespace, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the stubs for the objects in the given namespace that match
|
||||||
|
* the criteria from the given options.
|
||||||
|
*
|
||||||
|
* @param client the client
|
||||||
|
* @param namespace the namespace
|
||||||
|
* @param options the options
|
||||||
|
* @return the collection
|
||||||
|
* @throws ApiException the api exception
|
||||||
|
*/
|
||||||
|
public static Collection<K8sV1ServiceStub> list(K8sClient client,
|
||||||
|
String namespace, ListOptions options) throws ApiException {
|
||||||
|
return K8sGenericStub.list(V1Service.class, V1ServiceList.class, client,
|
||||||
|
CONTEXT, namespace, options, K8sV1ServiceStub::getGeneric);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide {@link GenericSupplier}.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({ "PMD.UnusedFormalParameter",
|
||||||
|
"PMD.UnusedPrivateMethod" })
|
||||||
|
private static K8sV1ServiceStub getGeneric(Class<V1Service> objectClass,
|
||||||
|
Class<V1ServiceList> objectListClass, K8sClient client,
|
||||||
|
APIResource context, String namespace, String name) {
|
||||||
|
return new K8sV1ServiceStub(client, namespace, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -11,4 +11,5 @@ plugins {
|
||||||
dependencies {
|
dependencies {
|
||||||
api 'org.jgrapes:org.jgrapes.core:[1.19.0,2)'
|
api 'org.jgrapes:org.jgrapes.core:[1.19.0,2)'
|
||||||
api project(':org.jdrupes.vmoperator.common')
|
api project(':org.jdrupes.vmoperator.common')
|
||||||
|
api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:[2.16.1,3]'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ public class ChannelCache<K, C extends Channel, A> {
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("PMD.ShortClassName")
|
@SuppressWarnings("PMD.ShortClassName")
|
||||||
private static class Data<C extends Channel, A> {
|
private static class Data<C extends Channel, A> {
|
||||||
public WeakReference<C> channel;
|
public final WeakReference<C> channel;
|
||||||
public A associated;
|
public A associated;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ import org.jgrapes.core.Event;
|
||||||
* Indicates that a display secret has changed.
|
* Indicates that a display secret has changed.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("PMD.DataClass")
|
@SuppressWarnings("PMD.DataClass")
|
||||||
public class DisplaySecretChanged extends Event<Void> {
|
public class DisplayPasswordChanged extends Event<Void> {
|
||||||
|
|
||||||
private final ResponseType type;
|
private final ResponseType type;
|
||||||
private final V1Secret secret;
|
private final V1Secret secret;
|
||||||
|
|
@ -39,7 +39,7 @@ public class DisplaySecretChanged extends Event<Void> {
|
||||||
* @param type the type
|
* @param type the type
|
||||||
* @param secret the secret
|
* @param secret the secret
|
||||||
*/
|
*/
|
||||||
public DisplaySecretChanged(ResponseType type, V1Secret secret) {
|
public DisplayPasswordChanged(ResponseType type, V1Secret secret) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.secret = secret;
|
this.secret = secret;
|
||||||
}
|
}
|
||||||
|
|
@ -68,8 +68,7 @@ public class DisplaySecretChanged extends Event<Void> {
|
||||||
builder.append(Components.objectName(this)).append(" [")
|
builder.append(Components.objectName(this)).append(" [")
|
||||||
.append(secret.getMetadata().getName()).append(' ').append(type);
|
.append(secret.getMetadata().getName()).append(' ').append(type);
|
||||||
if (channels() != null) {
|
if (channels() != null) {
|
||||||
builder.append(", channels=");
|
builder.append(", channels=").append(Channel.toString(channels()));
|
||||||
builder.append(Channel.toString(channels()));
|
|
||||||
}
|
}
|
||||||
builder.append(']');
|
builder.append(']');
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* VM-Operator
|
||||||
|
* Copyright (C) 2024 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
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jdrupes.vmoperator.manager.events;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.jgrapes.core.Event;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current display secret.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("PMD.DataClass")
|
||||||
|
public class GetDisplayPassword extends Event<String> {
|
||||||
|
|
||||||
|
private final String vmName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new returns the display secret.
|
||||||
|
*
|
||||||
|
* @param vmName the vm name
|
||||||
|
*/
|
||||||
|
public GetDisplayPassword(String vmName) {
|
||||||
|
this.vmName = vmName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the vm name.
|
||||||
|
*
|
||||||
|
* @return the vm name
|
||||||
|
*/
|
||||||
|
public String vmName() {
|
||||||
|
return vmName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the password. Should only be called when the event is completed.
|
||||||
|
*
|
||||||
|
* @return the optional
|
||||||
|
*/
|
||||||
|
public Optional<String> password() {
|
||||||
|
return currentResults().stream().findFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
/*
|
||||||
|
* VM-Operator
|
||||||
|
* Copyright (C) 2024 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
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jdrupes.vmoperator.manager.events;
|
||||||
|
|
||||||
|
import io.kubernetes.client.openapi.models.V1Service;
|
||||||
|
import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
|
||||||
|
import org.jgrapes.core.Channel;
|
||||||
|
import org.jgrapes.core.Components;
|
||||||
|
import org.jgrapes.core.Event;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that a service has changed.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("PMD.DataClass")
|
||||||
|
public class ServiceChanged extends Event<Void> {
|
||||||
|
|
||||||
|
private final ResponseType type;
|
||||||
|
private final V1Service service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a new service changed event.
|
||||||
|
*
|
||||||
|
* @param type the type
|
||||||
|
* @param service the service
|
||||||
|
*/
|
||||||
|
public ServiceChanged(ResponseType type, V1Service service) {
|
||||||
|
this.type = type;
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the type.
|
||||||
|
*
|
||||||
|
* @return the type
|
||||||
|
*/
|
||||||
|
public ResponseType type() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the service.
|
||||||
|
*
|
||||||
|
* @return the service
|
||||||
|
*/
|
||||||
|
public V1Service service() {
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
builder.append(Components.objectName(this)).append(" [")
|
||||||
|
.append(service.getMetadata().getName()).append(' ').append(type);
|
||||||
|
if (channels() != null) {
|
||||||
|
builder.append(", channels=").append(Channel.toString(channels()));
|
||||||
|
}
|
||||||
|
builder.append(']');
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -83,8 +83,7 @@ public class VmDefChanged extends Event<Void> {
|
||||||
builder.append(Components.objectName(this)).append(" [")
|
builder.append(Components.objectName(this)).append(" [")
|
||||||
.append(vmDef.getMetadata().getName()).append(' ').append(type);
|
.append(vmDef.getMetadata().getName()).append(' ').append(type);
|
||||||
if (channels() != null) {
|
if (channels() != null) {
|
||||||
builder.append(", channels=");
|
builder.append(", channels=").append(Channel.toString(channels()));
|
||||||
builder.append(Channel.toString(channels()));
|
|
||||||
}
|
}
|
||||||
builder.append(']');
|
builder.append(']');
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ dependencies {
|
||||||
runtimeOnly 'org.apache.logging.log4j:log4j-to-jul:2.20.0'
|
runtimeOnly 'org.apache.logging.log4j:log4j-to-jul:2.20.0'
|
||||||
|
|
||||||
runtimeOnly project(':org.jdrupes.vmoperator.vmconlet')
|
runtimeOnly project(':org.jdrupes.vmoperator.vmconlet')
|
||||||
|
runtimeOnly project(':org.jdrupes.vmoperator.vmviewer')
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@ data:
|
||||||
ticket: "${ cr.spec.vm.display.spice.ticket.asString }"
|
ticket: "${ cr.spec.vm.display.spice.ticket.asString }"
|
||||||
</#if>
|
</#if>
|
||||||
<#if cr.spec.vm.display.spice.streamingVideo??>
|
<#if cr.spec.vm.display.spice.streamingVideo??>
|
||||||
ticket: "${ cr.spec.vm.display.spice.streamingVideo.asString }"
|
streaming-video: "${ cr.spec.vm.display.spice.streamingVideo.asString }"
|
||||||
</#if>
|
</#if>
|
||||||
usbRedirects: ${ cr.spec.vm.display.spice.usbRedirects.asInt?c }
|
usbRedirects: ${ cr.spec.vm.display.spice.usbRedirects.asInt?c }
|
||||||
</#if>
|
</#if>
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
.list(newCm.getMetadata().getNamespace(), listOpts).getObject();
|
.list(newCm.getMetadata().getNamespace(), listOpts).getObject();
|
||||||
|
|
||||||
// If the VM is being created, the pod may not exist yet.
|
// If the VM is being created, the pod may not exist yet.
|
||||||
if (pods == null || pods.getItems().size() == 0) {
|
if (pods == null || pods.getItems().isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var pod = pods.getItems().get(0);
|
var pod = pods.getItems().get(0);
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ package org.jdrupes.vmoperator.manager;
|
||||||
/**
|
/**
|
||||||
* Some constants.
|
* Some constants.
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("PMD.DataClass")
|
||||||
public class Constants extends org.jdrupes.vmoperator.common.Constants {
|
public class Constants extends org.jdrupes.vmoperator.common.Constants {
|
||||||
|
|
||||||
/** The Constant COMP_DISPLAY_SECRET. */
|
/** The Constant COMP_DISPLAY_SECRET. */
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,7 @@ public class Controller extends Component {
|
||||||
/**
|
/**
|
||||||
* Creates a new instance.
|
* Creates a new instance.
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
|
||||||
public Controller(Channel componentChannel) {
|
public Controller(Channel componentChannel) {
|
||||||
super(componentChannel);
|
super(componentChannel);
|
||||||
// Prepare component tree
|
// Prepare component tree
|
||||||
|
|
@ -100,8 +101,11 @@ public class Controller extends Component {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
attach(new VmMonitor(channel()).channelManager(chanMgr));
|
attach(new VmMonitor(channel()).channelManager(chanMgr));
|
||||||
attach(new DisplaySecretsMonitor(channel())
|
attach(new DisplayPasswordMonitor(channel())
|
||||||
.channelManager(chanMgr.fixed()));
|
.channelManager(chanMgr.fixed()));
|
||||||
|
// Currently, we don't use the IP assigned by the load balancer
|
||||||
|
// to access the VM's console. Might change in the future.
|
||||||
|
// attach(new ServiceMonitor(channel()).channelManager(chanMgr));
|
||||||
attach(new Reconciler(channel()));
|
attach(new Reconciler(channel()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
* VM-Operator
|
||||||
|
* Copyright (C) 2024 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
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jdrupes.vmoperator.manager;
|
||||||
|
|
||||||
|
import io.kubernetes.client.openapi.ApiException;
|
||||||
|
import io.kubernetes.client.openapi.models.V1Secret;
|
||||||
|
import io.kubernetes.client.openapi.models.V1SecretList;
|
||||||
|
import io.kubernetes.client.util.Watch.Response;
|
||||||
|
import io.kubernetes.client.util.generic.options.ListOptions;
|
||||||
|
import java.io.IOException;
|
||||||
|
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
|
||||||
|
import org.jdrupes.vmoperator.common.K8sClient;
|
||||||
|
import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
|
||||||
|
import org.jdrupes.vmoperator.common.K8sV1SecretStub;
|
||||||
|
import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET;
|
||||||
|
import org.jdrupes.vmoperator.manager.events.DisplayPasswordChanged;
|
||||||
|
import org.jdrupes.vmoperator.manager.events.GetDisplayPassword;
|
||||||
|
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||||
|
import org.jgrapes.core.Channel;
|
||||||
|
import org.jgrapes.core.annotation.Handler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watches for changes of display secrets.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
||||||
|
public class DisplayPasswordMonitor
|
||||||
|
extends AbstractMonitor<V1Secret, V1SecretList, VmChannel> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new display secrets monitor.
|
||||||
|
*
|
||||||
|
* @param componentChannel the component channel
|
||||||
|
*/
|
||||||
|
public DisplayPasswordMonitor(Channel componentChannel) {
|
||||||
|
super(componentChannel, V1Secret.class, V1SecretList.class);
|
||||||
|
context(K8sV1SecretStub.CONTEXT);
|
||||||
|
ListOptions options = new ListOptions();
|
||||||
|
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
|
||||||
|
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET);
|
||||||
|
options(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void prepareMonitoring() throws IOException, ApiException {
|
||||||
|
client(new K8sClient());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void handleChange(K8sClient client, Response<V1Secret> change) {
|
||||||
|
String vmName = change.object.getMetadata().getLabels()
|
||||||
|
.get("app.kubernetes.io/instance");
|
||||||
|
if (vmName == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var channel = channel(vmName).orElse(null);
|
||||||
|
if (channel == null || channel.vmDefinition() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
channel.pipeline().fire(new DisplayPasswordChanged(
|
||||||
|
ResponseType.valueOf(change.type), change.object), channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On get display secrets.
|
||||||
|
*
|
||||||
|
* @param event the event
|
||||||
|
* @param channel the channel
|
||||||
|
* @throws ApiException the api exception
|
||||||
|
*/
|
||||||
|
@Handler
|
||||||
|
@SuppressWarnings("PMD.StringInstantiation")
|
||||||
|
public void onGetDisplaySecrets(GetDisplayPassword event, VmChannel channel)
|
||||||
|
throws ApiException {
|
||||||
|
ListOptions options = new ListOptions();
|
||||||
|
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
|
||||||
|
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET + ","
|
||||||
|
+ "app.kubernetes.io/instance=" + event.vmName());
|
||||||
|
var stubs = K8sV1SecretStub.list(client(), namespace(), options);
|
||||||
|
if (stubs.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stubs.iterator().next().model().map(m -> m.getData())
|
||||||
|
.map(m -> m.get("display-password"))
|
||||||
|
.ifPresent(p -> event.setResult(new String(p)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -224,6 +224,7 @@ public class Reconciler extends Component {
|
||||||
return new DynamicKubernetesObject(json);
|
return new DynamicKubernetesObject(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
|
||||||
private void adjustCdRomPaths(JsonObject json) {
|
private void adjustCdRomPaths(JsonObject json) {
|
||||||
var disks
|
var disks
|
||||||
= GsonPtr.to(json).to("spec", "vm", "disks").get(JsonArray.class);
|
= GsonPtr.to(json).to("spec", "vm", "disks").get(JsonArray.class);
|
||||||
|
|
|
||||||
|
|
@ -19,38 +19,36 @@
|
||||||
package org.jdrupes.vmoperator.manager;
|
package org.jdrupes.vmoperator.manager;
|
||||||
|
|
||||||
import io.kubernetes.client.openapi.ApiException;
|
import io.kubernetes.client.openapi.ApiException;
|
||||||
import io.kubernetes.client.openapi.models.V1Secret;
|
import io.kubernetes.client.openapi.models.V1Service;
|
||||||
import io.kubernetes.client.openapi.models.V1SecretList;
|
import io.kubernetes.client.openapi.models.V1ServiceList;
|
||||||
import io.kubernetes.client.util.Watch.Response;
|
import io.kubernetes.client.util.Watch.Response;
|
||||||
import io.kubernetes.client.util.generic.options.ListOptions;
|
import io.kubernetes.client.util.generic.options.ListOptions;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
|
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
|
||||||
import org.jdrupes.vmoperator.common.K8sClient;
|
import org.jdrupes.vmoperator.common.K8sClient;
|
||||||
import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
|
import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
|
||||||
import org.jdrupes.vmoperator.common.K8sV1SecretStub;
|
import org.jdrupes.vmoperator.common.K8sV1ServiceStub;
|
||||||
import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET;
|
import org.jdrupes.vmoperator.manager.events.ServiceChanged;
|
||||||
import org.jdrupes.vmoperator.manager.events.DisplaySecretChanged;
|
|
||||||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||||
import org.jgrapes.core.Channel;
|
import org.jgrapes.core.Channel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Watches for changes of display secrets.
|
* Watches for changes of services.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
||||||
public class DisplaySecretsMonitor
|
public class ServiceMonitor
|
||||||
extends AbstractMonitor<V1Secret, V1SecretList, VmChannel> {
|
extends AbstractMonitor<V1Service, V1ServiceList, VmChannel> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a new display secrets monitor.
|
* Instantiates a new display secrets monitor.
|
||||||
*
|
*
|
||||||
* @param componentChannel the component channel
|
* @param componentChannel the component channel
|
||||||
*/
|
*/
|
||||||
public DisplaySecretsMonitor(Channel componentChannel) {
|
public ServiceMonitor(Channel componentChannel) {
|
||||||
super(componentChannel, V1Secret.class, V1SecretList.class);
|
super(componentChannel, V1Service.class, V1ServiceList.class);
|
||||||
context(K8sV1SecretStub.CONTEXT);
|
context(K8sV1ServiceStub.CONTEXT);
|
||||||
ListOptions options = new ListOptions();
|
ListOptions options = new ListOptions();
|
||||||
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
|
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME);
|
||||||
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET);
|
|
||||||
options(options);
|
options(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,7 +58,7 @@ public class DisplaySecretsMonitor
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void handleChange(K8sClient client, Response<V1Secret> change) {
|
protected void handleChange(K8sClient client, Response<V1Service> change) {
|
||||||
String vmName = change.object.getMetadata().getLabels()
|
String vmName = change.object.getMetadata().getLabels()
|
||||||
.get("app.kubernetes.io/instance");
|
.get("app.kubernetes.io/instance");
|
||||||
if (vmName == null) {
|
if (vmName == null) {
|
||||||
|
|
@ -70,8 +68,7 @@ public class DisplaySecretsMonitor
|
||||||
if (channel == null || channel.vmDefinition() == null) {
|
if (channel == null || channel.vmDefinition() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
channel.pipeline().fire(new DisplaySecretChanged(
|
channel.pipeline().fire(new ServiceChanged(
|
||||||
ResponseType.valueOf(change.type), change.object), channel);
|
ResponseType.valueOf(change.type), change.object), channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -82,14 +82,15 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
// or not running.
|
// or not running.
|
||||||
var stsStub = K8sV1StatefulSetStub.get(channel.client(),
|
var stsStub = K8sV1StatefulSetStub.get(channel.client(),
|
||||||
metadata.getNamespace(), metadata.getName());
|
metadata.getNamespace(), metadata.getName());
|
||||||
stsStub.model().ifPresent(sts -> {
|
var stsModel = stsStub.model().orElse(null);
|
||||||
var current = sts.getSpec().getReplicas();
|
if (stsModel != null) {
|
||||||
|
var current = stsModel.getSpec().getReplicas();
|
||||||
var desired = GsonPtr.to(stsDef.getRaw())
|
var desired = GsonPtr.to(stsDef.getRaw())
|
||||||
.to("spec").getAsInt("replicas").orElse(1);
|
.to("spec").getAsInt("replicas").orElse(1);
|
||||||
if (current == 1 && desired == 1) {
|
if (current == 1 && desired == 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
// Do apply changes
|
// Do apply changes
|
||||||
PatchOptions opts = new PatchOptions();
|
PatchOptions opts = new PatchOptions();
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,7 @@ public class VmMonitor
|
||||||
private void addDynamicData(K8sClient client, K8sDynamicModel vmState) {
|
private void addDynamicData(K8sClient client, K8sDynamicModel vmState) {
|
||||||
var rootNode = GsonPtr.to(vmState.data()).get(JsonObject.class);
|
var rootNode = GsonPtr.to(vmState.data()).get(JsonObject.class);
|
||||||
rootNode.addProperty("nodeName", "");
|
rootNode.addProperty("nodeName", "");
|
||||||
|
rootNode.addProperty("nodeAddress", "");
|
||||||
|
|
||||||
// VM definition status changes before the pod terminates.
|
// VM definition status changes before the pod terminates.
|
||||||
// This results in pod information being shown for a stopped
|
// This results in pod information being shown for a stopped
|
||||||
|
|
@ -172,8 +173,17 @@ public class VmMonitor
|
||||||
var podList
|
var podList
|
||||||
= K8sV1PodStub.list(client, namespace(), podSearch);
|
= K8sV1PodStub.list(client, namespace(), podSearch);
|
||||||
for (var podStub : podList) {
|
for (var podStub : podList) {
|
||||||
rootNode.addProperty("nodeName",
|
var nodeName = podStub.model().get().getSpec().getNodeName();
|
||||||
podStub.model().get().getSpec().getNodeName());
|
rootNode.addProperty("nodeName", nodeName);
|
||||||
|
logger.fine(() -> "Added node name " + nodeName
|
||||||
|
+ " to VM info for " + vmState.getMetadata().getName());
|
||||||
|
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
|
||||||
|
var addrs = new JsonArray();
|
||||||
|
podStub.model().get().getStatus().getPodIPs().stream()
|
||||||
|
.map(ip -> ip.getIp()).forEach(addrs::add);
|
||||||
|
rootNode.add("nodeAddresses", addrs);
|
||||||
|
logger.fine(() -> "Added node addresses " + addrs
|
||||||
|
+ " to VM info for " + vmState.getMetadata().getName());
|
||||||
}
|
}
|
||||||
} catch (ApiException e) {
|
} catch (ApiException e) {
|
||||||
logger.log(Level.WARNING, e,
|
logger.log(Level.WARNING, e,
|
||||||
|
|
|
||||||
1
org.jdrupes.vmoperator.runner.qemu/password-expiry
Normal file
1
org.jdrupes.vmoperator.runner.qemu/password-expiry
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
+30
|
||||||
|
|
@ -108,7 +108,7 @@ public class Configuration implements Dto {
|
||||||
* Subsection "vm".
|
* Subsection "vm".
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({ "PMD.ShortClassName", "PMD.TooManyFields",
|
@SuppressWarnings({ "PMD.ShortClassName", "PMD.TooManyFields",
|
||||||
"PMD.DataClass" })
|
"PMD.DataClass", "PMD.AvoidDuplicateLiterals" })
|
||||||
public static class Vm implements Dto {
|
public static class Vm implements Dto {
|
||||||
|
|
||||||
/** The name. */
|
/** The name. */
|
||||||
|
|
@ -196,6 +196,7 @@ public class Configuration implements Dto {
|
||||||
/**
|
/**
|
||||||
* Subsection "network".
|
* Subsection "network".
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("PMD.DataClass")
|
||||||
public static class Network implements Dto {
|
public static class Network implements Dto {
|
||||||
|
|
||||||
/** The type. */
|
/** The type. */
|
||||||
|
|
@ -217,6 +218,7 @@ public class Configuration implements Dto {
|
||||||
/**
|
/**
|
||||||
* Subsection "drive".
|
* Subsection "drive".
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("PMD.DataClass")
|
||||||
public static class Drive implements Dto {
|
public static class Drive implements Dto {
|
||||||
|
|
||||||
/** The type. */
|
/** The type. */
|
||||||
|
|
@ -247,6 +249,7 @@ public class Configuration implements Dto {
|
||||||
/**
|
/**
|
||||||
* Subsection "spice".
|
* Subsection "spice".
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("PMD.DataClass")
|
||||||
public static class Spice implements Dto {
|
public static class Spice implements Dto {
|
||||||
|
|
||||||
/** The port. */
|
/** The port. */
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.commands.QmpAddCpu;
|
import org.jdrupes.vmoperator.runner.qemu.commands.QmpAddCpu;
|
||||||
|
|
@ -170,7 +171,7 @@ public class CpuController extends Component {
|
||||||
|
|
||||||
private void checkCpus() {
|
private void checkCpus() {
|
||||||
if (suspendedConfigure != null && desiredCpus != null
|
if (suspendedConfigure != null && desiredCpus != null
|
||||||
&& currentCpus == desiredCpus.intValue()) {
|
&& Objects.equals(currentCpus, desiredCpus)) {
|
||||||
suspendedConfigure.resumeHandling();
|
suspendedConfigure.resumeHandling();
|
||||||
suspendedConfigure = null;
|
suspendedConfigure = null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import java.nio.file.Path;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetDisplayPassword;
|
import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetDisplayPassword;
|
||||||
|
import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetPasswordExpiry;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu;
|
import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand;
|
import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State;
|
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State;
|
||||||
|
|
@ -40,6 +41,7 @@ import org.jgrapes.util.events.WatchFile;
|
||||||
public class DisplayController extends Component {
|
public class DisplayController extends Component {
|
||||||
|
|
||||||
public static final String DISPLAY_PASSWORD_FILE = "display-password";
|
public static final String DISPLAY_PASSWORD_FILE = "display-password";
|
||||||
|
public static final String PASSWORD_EXPIRY_FILE = "password-expiry";
|
||||||
private String currentPassword;
|
private String currentPassword;
|
||||||
private String protocol;
|
private String protocol;
|
||||||
private final Path configDir;
|
private final Path configDir;
|
||||||
|
|
@ -50,7 +52,8 @@ public class DisplayController extends Component {
|
||||||
* @param componentChannel the component channel
|
* @param componentChannel the component channel
|
||||||
* @param configDir
|
* @param configDir
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("PMD.AssignmentToNonFinalStatic")
|
@SuppressWarnings({ "PMD.AssignmentToNonFinalStatic",
|
||||||
|
"PMD.ConstructorCallsOverridableMethod" })
|
||||||
public DisplayController(Channel componentChannel, Path configDir) {
|
public DisplayController(Channel componentChannel, Path configDir) {
|
||||||
super(componentChannel);
|
super(componentChannel);
|
||||||
this.configDir = configDir;
|
this.configDir = configDir;
|
||||||
|
|
@ -90,7 +93,12 @@ public class DisplayController extends Component {
|
||||||
if (protocol == null) {
|
if (protocol == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (setDisplayPassword()) {
|
||||||
|
setPasswordExpiry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean setDisplayPassword() {
|
||||||
String password;
|
String password;
|
||||||
Path dpPath = configDir.resolve(DISPLAY_PASSWORD_FILE);
|
Path dpPath = configDir.resolve(DISPLAY_PASSWORD_FILE);
|
||||||
if (dpPath.toFile().canRead()) {
|
if (dpPath.toFile().canRead()) {
|
||||||
|
|
@ -100,18 +108,37 @@ public class DisplayController extends Component {
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.log(Level.WARNING, e, () -> "Cannot read display"
|
logger.log(Level.WARNING, e, () -> "Cannot read display"
|
||||||
+ " password: " + e.getMessage());
|
+ " password: " + e.getMessage());
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.finer(() -> "No display password");
|
logger.finer(() -> "No display password");
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Objects.equals(this.currentPassword, password)) {
|
if (Objects.equals(this.currentPassword, password)) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
logger.fine(() -> "Updating display password");
|
logger.fine(() -> "Updating display password");
|
||||||
fire(new MonitorCommand(new QmpSetDisplayPassword(protocol, password)));
|
fire(new MonitorCommand(new QmpSetDisplayPassword(protocol, password)));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setPasswordExpiry() {
|
||||||
|
Path pePath = configDir.resolve(PASSWORD_EXPIRY_FILE);
|
||||||
|
if (!pePath.toFile().canRead()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.finer(() -> "Found expiry time");
|
||||||
|
String expiry;
|
||||||
|
try {
|
||||||
|
expiry = Files.readString(pePath);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.log(Level.WARNING, e, () -> "Cannot read expiry"
|
||||||
|
+ " time: " + e.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.fine(() -> "Updating expiry time");
|
||||||
|
fire(new MonitorCommand(new QmpSetPasswordExpiry(protocol, expiry)));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,8 @@ public class QemuMonitor extends Component {
|
||||||
* @param configDir the config dir
|
* @param configDir the config dir
|
||||||
* @throws IOException Signals that an I/O exception has occurred.
|
* @throws IOException Signals that an I/O exception has occurred.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("PMD.AssignmentToNonFinalStatic")
|
@SuppressWarnings({ "PMD.AssignmentToNonFinalStatic",
|
||||||
|
"PMD.ConstructorCallsOverridableMethod" })
|
||||||
public QemuMonitor(Channel componentChannel, Path configDir)
|
public QemuMonitor(Channel componentChannel, Path configDir)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
super(componentChannel);
|
super(componentChannel);
|
||||||
|
|
@ -155,6 +156,7 @@ public class QemuMonitor extends Component {
|
||||||
* @param event the event
|
* @param event the event
|
||||||
* @param channel the channel
|
* @param channel the channel
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("resource")
|
||||||
@Handler
|
@Handler
|
||||||
public void onClientConnected(ClientConnected event,
|
public void onClientConnected(ClientConnected event,
|
||||||
SocketIOChannel channel) {
|
SocketIOChannel channel) {
|
||||||
|
|
@ -276,7 +278,7 @@ public class QemuMonitor extends Component {
|
||||||
writer.append(asText).append('\n').flush();
|
writer.append(asText).append('\n').flush();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// Cannot happen, but...
|
// Cannot happen, but...
|
||||||
logger.log(Level.WARNING, e, () -> e.getMessage());
|
logger.log(Level.WARNING, e, e::getMessage);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,8 @@ import org.jgrapes.util.events.WatchFile;
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({ "PMD.ExcessiveImports", "PMD.AvoidPrintStackTrace",
|
@SuppressWarnings({ "PMD.ExcessiveImports", "PMD.AvoidPrintStackTrace",
|
||||||
"PMD.DataflowAnomalyAnalysis", "PMD.TooManyMethods" })
|
"PMD.DataflowAnomalyAnalysis", "PMD.TooManyMethods",
|
||||||
|
"PMD.CouplingBetweenObjects" })
|
||||||
public class Runner extends Component {
|
public class Runner extends Component {
|
||||||
|
|
||||||
private static final String QEMU = "qemu";
|
private static final String QEMU = "qemu";
|
||||||
|
|
@ -232,7 +233,8 @@ public class Runner extends Component {
|
||||||
* @param cmdLine the cmd line
|
* @param cmdLine the cmd line
|
||||||
* @throws IOException Signals that an I/O exception has occurred.
|
* @throws IOException Signals that an I/O exception has occurred.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("PMD.SystemPrintln")
|
@SuppressWarnings({ "PMD.SystemPrintln",
|
||||||
|
"PMD.ConstructorCallsOverridableMethod" })
|
||||||
public Runner(CommandLine cmdLine) throws IOException {
|
public Runner(CommandLine cmdLine) throws IOException {
|
||||||
yamlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
|
yamlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
|
||||||
false);
|
false);
|
||||||
|
|
@ -495,27 +497,27 @@ public class Runner extends Component {
|
||||||
try {
|
try {
|
||||||
var cloudInitDir = config.dataDir.resolve("cloud-init");
|
var cloudInitDir = config.dataDir.resolve("cloud-init");
|
||||||
cloudInitDir.toFile().mkdir();
|
cloudInitDir.toFile().mkdir();
|
||||||
var metaOut
|
try (var metaOut
|
||||||
= Files.newBufferedWriter(cloudInitDir.resolve("meta-data"));
|
= Files.newBufferedWriter(cloudInitDir.resolve("meta-data"))) {
|
||||||
if (config.cloudInit.metaData != null) {
|
if (config.cloudInit.metaData != null) {
|
||||||
yamlMapper.writer().writeValue(metaOut,
|
yamlMapper.writer().writeValue(metaOut,
|
||||||
config.cloudInit.metaData);
|
config.cloudInit.metaData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
metaOut.close();
|
try (var userOut
|
||||||
var userOut
|
= Files.newBufferedWriter(cloudInitDir.resolve("user-data"))) {
|
||||||
= Files.newBufferedWriter(cloudInitDir.resolve("user-data"));
|
userOut.write("#cloud-config\n");
|
||||||
userOut.write("#cloud-config\n");
|
if (config.cloudInit.userData != null) {
|
||||||
if (config.cloudInit.userData != null) {
|
yamlMapper.writer().writeValue(userOut,
|
||||||
yamlMapper.writer().writeValue(userOut,
|
config.cloudInit.userData);
|
||||||
config.cloudInit.userData);
|
}
|
||||||
}
|
}
|
||||||
userOut.close();
|
|
||||||
if (config.cloudInit.networkConfig != null) {
|
if (config.cloudInit.networkConfig != null) {
|
||||||
var networkConfig = Files.newBufferedWriter(
|
try (var networkConfig = Files.newBufferedWriter(
|
||||||
cloudInitDir.resolve("network-config"));
|
cloudInitDir.resolve("network-config"))) {
|
||||||
yamlMapper.writer().writeValue(networkConfig,
|
yamlMapper.writer().writeValue(networkConfig,
|
||||||
config.cloudInit.networkConfig);
|
config.cloudInit.networkConfig);
|
||||||
networkConfig.close();
|
}
|
||||||
}
|
}
|
||||||
startProcess(cloudInitImgDefinition);
|
startProcess(cloudInitImgDefinition);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|
@ -545,7 +547,6 @@ public class Runner extends Component {
|
||||||
&& event.path().equals(config.swtpmSocket)) {
|
&& event.path().equals(config.swtpmSocket)) {
|
||||||
// swtpm running, maybe start qemu
|
// swtpm running, maybe start qemu
|
||||||
mayBeStartQemu(QemuPreps.Tpm);
|
mayBeStartQemu(QemuPreps.Tpm);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -690,6 +691,7 @@ public class Runner extends Component {
|
||||||
"The VM has been shut down"));
|
"The VM has been shut down"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("PMD.ConfusingArgumentToVarargsMethod")
|
||||||
private void shutdown() {
|
private void shutdown() {
|
||||||
if (!Set.of(State.TERMINATING, State.STOPPED).contains(state)) {
|
if (!Set.of(State.TERMINATING, State.STOPPED).contains(state)) {
|
||||||
fire(new Stop());
|
fire(new Stop());
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,7 @@ public class StatusUpdater extends Component {
|
||||||
*
|
*
|
||||||
* @param componentChannel the component channel
|
* @param componentChannel the component channel
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
|
||||||
public StatusUpdater(Channel componentChannel) {
|
public StatusUpdater(Channel componentChannel) {
|
||||||
super(componentChannel);
|
super(componentChannel);
|
||||||
try {
|
try {
|
||||||
|
|
@ -91,7 +92,6 @@ public class StatusUpdater extends Component {
|
||||||
() -> "Cannot access events API, terminating.");
|
() -> "Cannot access events API, terminating.");
|
||||||
fire(new Exit(1));
|
fire(new Exit(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -179,6 +179,7 @@ public class StatusUpdater extends Component {
|
||||||
* @throws ApiException
|
* @throws ApiException
|
||||||
*/
|
*/
|
||||||
@Handler
|
@Handler
|
||||||
|
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
|
||||||
public void onConfigureQemu(ConfigureQemu event)
|
public void onConfigureQemu(ConfigureQemu event)
|
||||||
throws ApiException {
|
throws ApiException {
|
||||||
guestShutdownStops = event.configuration().guestShutdownStops;
|
guestShutdownStops = event.configuration().guestShutdownStops;
|
||||||
|
|
@ -189,14 +190,22 @@ public class StatusUpdater extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// A change of the runner configuration is typically caused
|
// A change of the runner configuration is typically caused
|
||||||
// by a new version of the CR. So we observe the new CR.
|
// by a new version of the CR. So we update only if we have
|
||||||
|
// a new version of the CR. There's one exception: the display
|
||||||
|
// password is configured by a file, not by the CR.
|
||||||
var vmDef = vmStub.model();
|
var vmDef = vmStub.model();
|
||||||
if (vmDef.isPresent()
|
if (vmDef.isPresent()
|
||||||
&& vmDef.get().metadata().getGeneration() == observedGeneration) {
|
&& vmDef.get().metadata().getGeneration() == observedGeneration
|
||||||
|
&& (event.configuration().hasDisplayPassword
|
||||||
|
|| vmDef.get().status().getAsJsonPrimitive(
|
||||||
|
"displayPasswordSerial").getAsInt() == -1)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
vmStub.updateStatus(vmDef.get(), from -> {
|
vmStub.updateStatus(vmDef.get(), from -> {
|
||||||
JsonObject status = from.status();
|
JsonObject status = from.status();
|
||||||
|
if (!event.configuration().hasDisplayPassword) {
|
||||||
|
status.addProperty("displayPasswordSerial", -1);
|
||||||
|
}
|
||||||
status.getAsJsonArray("conditions").asList().stream()
|
status.getAsJsonArray("conditions").asList().stream()
|
||||||
.map(cond -> (JsonObject) cond).filter(cond -> "Running"
|
.map(cond -> (JsonObject) cond).filter(cond -> "Running"
|
||||||
.equals(cond.get("type").getAsString()))
|
.equals(cond.get("type").getAsString()))
|
||||||
|
|
@ -213,7 +222,8 @@ public class StatusUpdater extends Component {
|
||||||
* @throws ApiException
|
* @throws ApiException
|
||||||
*/
|
*/
|
||||||
@Handler
|
@Handler
|
||||||
@SuppressWarnings("PMD.AssignmentInOperand")
|
@SuppressWarnings({ "PMD.AssignmentInOperand",
|
||||||
|
"PMD.AvoidLiteralsInIfCondition" })
|
||||||
public void onRunnerStateChanged(RunnerStateChange event)
|
public void onRunnerStateChanged(RunnerStateChange event)
|
||||||
throws ApiException {
|
throws ApiException {
|
||||||
K8sDynamicModel vmDef;
|
K8sDynamicModel vmDef;
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ public class QmpAddCpu extends QmpCommand {
|
||||||
cmd.put("execute", "device_add");
|
cmd.put("execute", "device_add");
|
||||||
ObjectNode args = mapper.createObjectNode();
|
ObjectNode args = mapper.createObjectNode();
|
||||||
cmd.set("arguments", args);
|
cmd.set("arguments", args);
|
||||||
args.setAll((ObjectNode) (unused.get("props")));
|
args.setAll((ObjectNode) unused.get("props"));
|
||||||
args.set("driver", unused.get("type"));
|
args.set("driver", unused.get("type"));
|
||||||
args.put("id", cpuId);
|
args.put("id", cpuId);
|
||||||
return cmd;
|
return cmd;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* VM-Operator
|
||||||
|
* Copyright (C) 2023 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
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jdrupes.vmoperator.runner.qemu.commands;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.TextNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link QmpCommand} that sets the password expiry.
|
||||||
|
*/
|
||||||
|
public class QmpSetPasswordExpiry extends QmpCommand {
|
||||||
|
|
||||||
|
private final String protocol;
|
||||||
|
private final String expiry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new command.
|
||||||
|
*
|
||||||
|
* @param protocol the protocol
|
||||||
|
* @param expiry the expiry time
|
||||||
|
*/
|
||||||
|
public QmpSetPasswordExpiry(String protocol, String expiry) {
|
||||||
|
this.protocol = protocol;
|
||||||
|
this.expiry = expiry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JsonNode toJson() {
|
||||||
|
ObjectNode cmd = mapper.createObjectNode();
|
||||||
|
cmd.put("execute", "expire_password");
|
||||||
|
ObjectNode args = mapper.createObjectNode();
|
||||||
|
cmd.set("arguments", args);
|
||||||
|
args.set("protocol", new TextNode(protocol));
|
||||||
|
args.set("time", new TextNode(expiry));
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
try {
|
||||||
|
var json = toJson();
|
||||||
|
return mapper.writeValueAsString(json);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
return "(no string representation)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -30,7 +30,9 @@ import org.jdrupes.vmoperator.runner.qemu.commands.QmpCommand;
|
||||||
*/
|
*/
|
||||||
public class HotpluggableCpuStatus extends MonitorResult {
|
public class HotpluggableCpuStatus extends MonitorResult {
|
||||||
|
|
||||||
|
@SuppressWarnings("PMD.ImmutableField")
|
||||||
private List<ObjectNode> usedCpus = new ArrayList<>();
|
private List<ObjectNode> usedCpus = new ArrayList<>();
|
||||||
|
@SuppressWarnings("PMD.ImmutableField")
|
||||||
private List<ObjectNode> unusedCpus = new ArrayList<>();
|
private List<ObjectNode> unusedCpus = new ArrayList<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,7 @@ public class MonitorCommand extends Event<Void> {
|
||||||
builder.append(Components.objectName(this))
|
builder.append(Components.objectName(this))
|
||||||
.append(" [").append(command);
|
.append(" [").append(command);
|
||||||
if (channels() != null) {
|
if (channels() != null) {
|
||||||
builder.append(", channels=");
|
builder.append(", channels=").append(Channel.toString(channels()));
|
||||||
builder.append(Channel.toString(channels()));
|
|
||||||
}
|
}
|
||||||
builder.append(']');
|
builder.append(']');
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
|
|
|
||||||
|
|
@ -152,8 +152,7 @@ public class MonitorResult extends Event<Void> {
|
||||||
builder.append(Components.objectName(this))
|
builder.append(Components.objectName(this))
|
||||||
.append(" [").append(executed).append(", ").append(successful());
|
.append(" [").append(executed).append(", ").append(successful());
|
||||||
if (channels() != null) {
|
if (channels() != null) {
|
||||||
builder.append(", channels=");
|
builder.append(", channels=").append(Channel.toString(channels()));
|
||||||
builder.append(Channel.toString(channels()));
|
|
||||||
}
|
}
|
||||||
builder.append(']');
|
builder.append(']');
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
|
|
|
||||||
|
|
@ -109,15 +109,14 @@ public class RunnerStateChange extends Event<Void> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder(50);
|
||||||
builder.append(Components.objectName(this))
|
builder.append(Components.objectName(this))
|
||||||
.append(" [").append(state).append(": ").append(reason);
|
.append(" [").append(state).append(": ").append(reason);
|
||||||
if (failed) {
|
if (failed) {
|
||||||
builder.append(" (failed)");
|
builder.append(" (failed)");
|
||||||
}
|
}
|
||||||
if (channels() != null) {
|
if (channels() != null) {
|
||||||
builder.append(", channels=");
|
builder.append(", channels=").append(Channel.toString(channels()));
|
||||||
builder.append(Channel.toString(channels()));
|
|
||||||
}
|
}
|
||||||
builder.append(']');
|
builder.append(']');
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
conletName = VM Viewer
|
conletName = VM Infos
|
||||||
|
|
||||||
VMsSummary = VMs (running/total)
|
VMsSummary = VMs (running/total)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
conletName = VM Anzeige
|
conletName = VM-Informationen
|
||||||
|
|
||||||
VMsSummary = VMs (gestartet/gesamt)
|
VMsSummary = VMs (gestartet/gesamt)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
* on by default and that {@link Manager#fire(Event, Channel...)}
|
* on by default and that {@link Manager#fire(Event, Channel...)}
|
||||||
* sends the event to
|
* sends the event to
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
|
||||||
public VmConlet(Channel componentChannel) {
|
public VmConlet(Channel componentChannel) {
|
||||||
super(componentChannel);
|
super(componentChannel);
|
||||||
setPeriodicRefresh(Duration.ofMinutes(1), () -> new Update());
|
setPeriodicRefresh(Duration.ofMinutes(1), () -> new Update());
|
||||||
|
|
@ -138,7 +139,7 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
.setRenderAs(
|
.setRenderAs(
|
||||||
RenderMode.Preview.addModifiers(event.renderAs()))
|
RenderMode.Preview.addModifiers(event.renderAs()))
|
||||||
.setSupportedModes(MODES));
|
.setSupportedModes(MODES));
|
||||||
renderedAs.add(RenderMode.View);
|
renderedAs.add(RenderMode.Preview);
|
||||||
channel.respond(new NotifyConletView(type(),
|
channel.respond(new NotifyConletView(type(),
|
||||||
conletId, "summarySeries", summarySeries.entries()));
|
conletId, "summarySeries", summarySeries.entries()));
|
||||||
var summary = evaluateSummary(false);
|
var summary = evaluateSummary(false);
|
||||||
|
|
@ -181,7 +182,8 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
*/
|
*/
|
||||||
@Handler(namedChannels = "manager")
|
@Handler(namedChannels = "manager")
|
||||||
@SuppressWarnings({ "PMD.ConfusingTernary", "PMD.CognitiveComplexity",
|
@SuppressWarnings({ "PMD.ConfusingTernary", "PMD.CognitiveComplexity",
|
||||||
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals" })
|
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals",
|
||||||
|
"PMD.ConfusingArgumentToVarargsMethod" })
|
||||||
public void onVmDefChanged(VmDefChanged event, VmChannel channel)
|
public void onVmDefChanged(VmDefChanged event, VmChannel channel)
|
||||||
throws JsonDecodeException, IOException {
|
throws JsonDecodeException, IOException {
|
||||||
var vmName = event.vmDefinition().getMetadata().getName();
|
var vmName = event.vmDefinition().getMetadata().getName();
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,11 @@
|
||||||
.jdrupes-vmoperator-vmconlet-view-action-list {
|
.jdrupes-vmoperator-vmconlet-view-action-list {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
[role=button]:not(:last-child) {
|
[role=button] {
|
||||||
margin-right: 0.5em;
|
padding: 0.25rem;
|
||||||
|
|
||||||
|
&:not([aria-disabled]):hover {
|
||||||
|
box-shadow: var(--darkening);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
org.jdrupes.vmoperator.vmviewer/.checkstyle
Normal file
10
org.jdrupes.vmoperator.vmviewer/.checkstyle
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<fileset-config file-format-version="1.2.0" simple-config="false" sync-formatter="false">
|
||||||
|
<local-check-config name="Project Checks" location="/VM-Operator/checkstyle.xml" type="project" description="">
|
||||||
|
<additional-data name="protect-config-file" value="false"/>
|
||||||
|
</local-check-config>
|
||||||
|
<fileset name="all" enabled="true" check-config-name="Project Checks" local="true">
|
||||||
|
<file-match-pattern match-pattern="." include-pattern="true"/>
|
||||||
|
</fileset>
|
||||||
|
</fileset-config>
|
||||||
7
org.jdrupes.vmoperator.vmviewer/.eclipse-pmd
Normal file
7
org.jdrupes.vmoperator.vmviewer/.eclipse-pmd
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<eclipse-pmd xmlns="http://acanda.ch/eclipse-pmd/0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://acanda.ch/eclipse-pmd/0.8 http://acanda.ch/eclipse-pmd/eclipse-pmd-0.8.xsd">
|
||||||
|
<analysis enabled="true" />
|
||||||
|
<rulesets>
|
||||||
|
<ruleset name="Custom Rules" ref="VM-Operator/ruleset.xml" refcontext="workspace" />
|
||||||
|
</rulesets>
|
||||||
|
</eclipse-pmd>
|
||||||
1
org.jdrupes.vmoperator.vmviewer/.eslintignore
Normal file
1
org.jdrupes.vmoperator.vmviewer/.eslintignore
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
rollup.config.mjs
|
||||||
15
org.jdrupes.vmoperator.vmviewer/.eslintrc.json
Normal file
15
org.jdrupes.vmoperator.vmviewer/.eslintrc.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended"
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": { "project": ["./tsconfig.json"] },
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"constructor-super": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
4
org.jdrupes.vmoperator.vmviewer/.gitignore
vendored
Normal file
4
org.jdrupes.vmoperator.vmviewer/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
/bin/
|
||||||
|
/bin_test/
|
||||||
|
/generated/
|
||||||
|
/build/
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
build.commands=org.eclipse.jdt.core.javabuilder
|
||||||
|
connection.arguments=
|
||||||
|
connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
|
||||||
|
connection.java.home=null
|
||||||
|
connection.jvm.arguments=
|
||||||
|
connection.project.dir=..
|
||||||
|
derived.resources=.gradle,generated
|
||||||
|
eclipse.preferences.version=1
|
||||||
|
natures=org.eclipse.jdt.groovy.core.groovyNature,org.eclipse.jdt.core.javanature
|
||||||
|
project.path=\:org.jgrapes.osgi.conlets.services
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
eclipse.preferences.version=1
|
||||||
|
encoding/<project>=UTF-8
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
eclipse.preferences.version=1
|
||||||
|
line.separator=\n
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
eclipse.preferences.version=1
|
||||||
|
editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
|
||||||
|
formatter_profile=_JGrapes
|
||||||
|
formatter_settings_version=13
|
||||||
|
sp_cleanup.add_default_serial_version_id=true
|
||||||
|
sp_cleanup.add_generated_serial_version_id=false
|
||||||
|
sp_cleanup.add_missing_annotations=true
|
||||||
|
sp_cleanup.add_missing_deprecated_annotations=true
|
||||||
|
sp_cleanup.add_missing_methods=false
|
||||||
|
sp_cleanup.add_missing_nls_tags=false
|
||||||
|
sp_cleanup.add_missing_override_annotations=true
|
||||||
|
sp_cleanup.add_missing_override_annotations_interface_methods=true
|
||||||
|
sp_cleanup.add_serial_version_id=false
|
||||||
|
sp_cleanup.always_use_blocks=true
|
||||||
|
sp_cleanup.always_use_parentheses_in_expressions=false
|
||||||
|
sp_cleanup.always_use_this_for_non_static_field_access=false
|
||||||
|
sp_cleanup.always_use_this_for_non_static_method_access=false
|
||||||
|
sp_cleanup.convert_functional_interfaces=false
|
||||||
|
sp_cleanup.convert_to_enhanced_for_loop=false
|
||||||
|
sp_cleanup.correct_indentation=false
|
||||||
|
sp_cleanup.format_source_code=true
|
||||||
|
sp_cleanup.format_source_code_changes_only=false
|
||||||
|
sp_cleanup.insert_inferred_type_arguments=false
|
||||||
|
sp_cleanup.make_local_variable_final=true
|
||||||
|
sp_cleanup.make_parameters_final=false
|
||||||
|
sp_cleanup.make_private_fields_final=true
|
||||||
|
sp_cleanup.make_type_abstract_if_missing_method=false
|
||||||
|
sp_cleanup.make_variable_declarations_final=false
|
||||||
|
sp_cleanup.never_use_blocks=false
|
||||||
|
sp_cleanup.never_use_parentheses_in_expressions=true
|
||||||
|
sp_cleanup.on_save_use_additional_actions=false
|
||||||
|
sp_cleanup.organize_imports=false
|
||||||
|
sp_cleanup.qualify_static_field_accesses_with_declaring_class=false
|
||||||
|
sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
|
||||||
|
sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
|
||||||
|
sp_cleanup.qualify_static_member_accesses_with_declaring_class=false
|
||||||
|
sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
|
||||||
|
sp_cleanup.remove_private_constructors=true
|
||||||
|
sp_cleanup.remove_redundant_type_arguments=false
|
||||||
|
sp_cleanup.remove_trailing_whitespaces=false
|
||||||
|
sp_cleanup.remove_trailing_whitespaces_all=true
|
||||||
|
sp_cleanup.remove_trailing_whitespaces_ignore_empty=false
|
||||||
|
sp_cleanup.remove_unnecessary_casts=true
|
||||||
|
sp_cleanup.remove_unnecessary_nls_tags=false
|
||||||
|
sp_cleanup.remove_unused_imports=false
|
||||||
|
sp_cleanup.remove_unused_local_variables=false
|
||||||
|
sp_cleanup.remove_unused_private_fields=true
|
||||||
|
sp_cleanup.remove_unused_private_members=false
|
||||||
|
sp_cleanup.remove_unused_private_methods=true
|
||||||
|
sp_cleanup.remove_unused_private_types=true
|
||||||
|
sp_cleanup.sort_members=false
|
||||||
|
sp_cleanup.sort_members_all=false
|
||||||
|
sp_cleanup.use_anonymous_class_creation=false
|
||||||
|
sp_cleanup.use_blocks=false
|
||||||
|
sp_cleanup.use_blocks_only_for_return_and_throw=false
|
||||||
|
sp_cleanup.use_lambda=true
|
||||||
|
sp_cleanup.use_parentheses_in_expressions=false
|
||||||
|
sp_cleanup.use_this_for_non_static_field_access=false
|
||||||
|
sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true
|
||||||
|
sp_cleanup.use_this_for_non_static_method_access=false
|
||||||
|
sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true
|
||||||
|
sp_jautodoc.cleanup.add_header=false
|
||||||
|
sp_jautodoc.cleanup.replace_header=false
|
||||||
57
org.jdrupes.vmoperator.vmviewer/build.gradle
Normal file
57
org.jdrupes.vmoperator.vmviewer/build.gradle
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
plugins {
|
||||||
|
id 'org.jdrupes.vmoperator.java-library-conventions'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(':org.jdrupes.vmoperator.manager.events')
|
||||||
|
|
||||||
|
implementation 'org.jgrapes:org.jgrapes.webconsole.base:[1.3.0,2)'
|
||||||
|
implementation 'org.jgrapes:org.jgrapes.webconsole.provider.vue:[1,2)'
|
||||||
|
implementation 'org.jgrapes:org.jgrapes.webconsole.provider.jgwcvuecomponents:[1.2,2)'
|
||||||
|
implementation 'org.jgrapes:org.jgrapes.webconsole.provider.chartjs:[1.2,2)'
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'com.github.node-gradle.node'
|
||||||
|
|
||||||
|
node {
|
||||||
|
download = true
|
||||||
|
}
|
||||||
|
|
||||||
|
task extractDependencies(type: Copy) {
|
||||||
|
from configurations.compileClasspath
|
||||||
|
.findAll{ it.name.contains('.provider.')
|
||||||
|
|| it.name.contains('org.jgrapes.webconsole.base')
|
||||||
|
}
|
||||||
|
.collect{ zipTree (it) }
|
||||||
|
exclude '*.class'
|
||||||
|
into 'build/unpacked'
|
||||||
|
duplicatesStrategy 'include'
|
||||||
|
}
|
||||||
|
|
||||||
|
task compileTs(type: NodeTask) {
|
||||||
|
dependsOn ':npmInstall'
|
||||||
|
dependsOn extractDependencies
|
||||||
|
inputs.dir project.file('src')
|
||||||
|
inputs.file project.file('tsconfig.json')
|
||||||
|
inputs.file project.file('rollup.config.mjs')
|
||||||
|
outputs.dir project.file('build/generated/resources')
|
||||||
|
script = file("${rootProject.rootDir}/node_modules/rollup/dist/bin/rollup")
|
||||||
|
args = ["-c"]
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
resources {
|
||||||
|
srcDir project.file('build/generated/resources')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processResources {
|
||||||
|
dependsOn compileTs
|
||||||
|
}
|
||||||
|
|
||||||
|
eclipse {
|
||||||
|
autoBuildTasks compileTs
|
||||||
|
}
|
||||||
1
org.jdrupes.vmoperator.vmviewer/package.json
Normal file
1
org.jdrupes.vmoperator.vmviewer/package.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
org.jdrupes.vmoperator.vmviewer.VmViewerFactory
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<div title="${_("conletName")}" class="jdrupes-vmoperator-vmviewer-edit"
|
||||||
|
data-jgwc-on-load="orgJDrupesVmOperatorVmViewer.initEdit"
|
||||||
|
data-jgwc-on-action="orgJDrupesVmOperatorVmViewer.applyEdit"
|
||||||
|
data-jgwc-on-unload="JGConsole.jgwc.unmountVueApps">
|
||||||
|
<form :id="formId" ref="formDom" onsubmit="return false;">
|
||||||
|
<section>
|
||||||
|
<span>{{ localize("Select VM") }}</span>
|
||||||
|
<p>
|
||||||
|
<label>
|
||||||
|
<span>{{ localize("VM") }}</span>
|
||||||
|
<select v-model="vmNameInput">
|
||||||
|
<#list vmNames as name>
|
||||||
|
<option value="${name}">${name}</option>
|
||||||
|
</#list>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* VM-Operator
|
||||||
|
* Copyright (C) 2024 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
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const l10nBundles = new Map();
|
||||||
|
let entries = null;
|
||||||
|
// <#list supportedLanguages() as l>
|
||||||
|
entries = new Map();
|
||||||
|
l10nBundles.set("${l.locale.toLanguageTag()}", entries);
|
||||||
|
// <#list l.l10nBundle.keys as key>
|
||||||
|
entries.set("${key}", "${l.l10nBundle.getString(key)}");
|
||||||
|
// </#list>
|
||||||
|
// </#list>
|
||||||
|
|
||||||
|
export default l10nBundles;
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<div class="jdrupes-vmoperator-vmviewer jdrupes-vmoperator-vmviewer-preview"
|
||||||
|
data-conlet-grid-rows="2" data-conlet-grid-columns="2"
|
||||||
|
data-jgwc-on-load="orgJDrupesVmOperatorVmViewer.initPreview"
|
||||||
|
data-jgwc-on-unload="JGConsole.jgwc.unmountVueApps"
|
||||||
|
data-conlet-resource-base="${conletResource('')}">
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
fill="#000000"
|
||||||
|
width="800"
|
||||||
|
height="533.33331"
|
||||||
|
viewBox="0 0 24 15.999999"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="computer-off.svg"
|
||||||
|
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1">
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="fillet_chamfer"
|
||||||
|
id="path-effect4"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
|
||||||
|
radius="0"
|
||||||
|
unit="px"
|
||||||
|
method="auto"
|
||||||
|
mode="F"
|
||||||
|
chamfer_steps="1"
|
||||||
|
flexible="false"
|
||||||
|
use_knot_distance="true"
|
||||||
|
apply_no_radius="true"
|
||||||
|
apply_with_radius="true"
|
||||||
|
only_selected="false"
|
||||||
|
hide_knots="false" />
|
||||||
|
<linearGradient
|
||||||
|
id="swatch3"
|
||||||
|
inkscape:swatch="solid">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#000000;stop-opacity:0;"
|
||||||
|
offset="0"
|
||||||
|
id="stop3" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="1.3435029"
|
||||||
|
inkscape:cx="377.74389"
|
||||||
|
inkscape:cy="227.01849"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1011"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="32"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg1" />
|
||||||
|
<path
|
||||||
|
id="rect1"
|
||||||
|
style="fill-opacity:1;stroke:#000000;stroke-width:1.97262;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:0;paint-order:fill markers stroke;fill:#545454"
|
||||||
|
d="M 3.0038192,0.98808897 H 20.99618 V 13.006705 H 3.0038192 Z" />
|
||||||
|
<rect
|
||||||
|
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.00306926;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
|
||||||
|
id="rect2"
|
||||||
|
width="23.995173"
|
||||||
|
height="2.0017407"
|
||||||
|
x="0.0039473679"
|
||||||
|
y="13.998839" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
fill="#000000"
|
||||||
|
width="800"
|
||||||
|
height="533.33331"
|
||||||
|
viewBox="0 0 24 15.999999"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="computer.svg"
|
||||||
|
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1">
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="fillet_chamfer"
|
||||||
|
id="path-effect4"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
|
||||||
|
radius="0"
|
||||||
|
unit="px"
|
||||||
|
method="auto"
|
||||||
|
mode="F"
|
||||||
|
chamfer_steps="1"
|
||||||
|
flexible="false"
|
||||||
|
use_knot_distance="true"
|
||||||
|
apply_no_radius="true"
|
||||||
|
apply_with_radius="true"
|
||||||
|
only_selected="false"
|
||||||
|
hide_knots="false" />
|
||||||
|
<linearGradient
|
||||||
|
id="swatch3"
|
||||||
|
inkscape:swatch="solid">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#000000;stop-opacity:0;"
|
||||||
|
offset="0"
|
||||||
|
id="stop3" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="1.3435029"
|
||||||
|
inkscape:cx="377.74389"
|
||||||
|
inkscape:cy="227.01849"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1011"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="32"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg1" />
|
||||||
|
<path
|
||||||
|
id="rect1"
|
||||||
|
style="fill-opacity:0;stroke:#000000;stroke-width:1.97262;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:0;paint-order:fill markers stroke"
|
||||||
|
d="M 3.0038192,0.98808897 H 20.99618 V 13.006705 H 3.0038192 Z" />
|
||||||
|
<rect
|
||||||
|
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.00306926;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
|
||||||
|
id="rect2"
|
||||||
|
width="23.995173"
|
||||||
|
height="2.0017407"
|
||||||
|
x="0.0039473679"
|
||||||
|
y="13.998839" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
conletName = VM Console
|
||||||
|
|
||||||
|
okayLabel = Apply and Close
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
conletName = VM-Konsole
|
||||||
|
|
||||||
|
okayLabel = Anwenden und Schließen
|
||||||
|
Select\ VM = VM auswählen
|
||||||
|
|
||||||
|
Start\ VM = VM Starten
|
||||||
|
Stop\ VM = VM Anhalten
|
||||||
35
org.jdrupes.vmoperator.vmviewer/rollup.config.mjs
Normal file
35
org.jdrupes.vmoperator.vmviewer/rollup.config.mjs
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import typescript from 'rollup-plugin-typescript2';
|
||||||
|
import postcss from 'rollup-plugin-postcss';
|
||||||
|
|
||||||
|
let packagePath = "org/jdrupes/vmoperator/vmviewer";
|
||||||
|
let baseName = "VmViewer"
|
||||||
|
let module = "build/generated/resources/" + packagePath
|
||||||
|
+ "/" + baseName + "-functions.js";
|
||||||
|
|
||||||
|
let pathsMap = {
|
||||||
|
"aash-plugin": "../../page-resource/aash-vue-components/lib/aash-vue-components.js",
|
||||||
|
"jgconsole": "../../console-base-resource/jgconsole.js",
|
||||||
|
"jgwc": "../../page-resource/jgwc-vue-components/jgwc-components.js",
|
||||||
|
"l10nBundles": "./" + baseName + "-l10nBundles.ftl.js",
|
||||||
|
"vue": "../../page-resource/vue/vue.esm-browser.js"
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
external: ['aash-plugin', 'jgconsole', 'jgwc', 'l10nBundles', 'vue', 'chartjs'],
|
||||||
|
input: "src/" + packagePath + "/browser/" + baseName + "-functions.ts",
|
||||||
|
output: [
|
||||||
|
{
|
||||||
|
format: "esm",
|
||||||
|
file: module,
|
||||||
|
sourcemap: true,
|
||||||
|
sourcemapPathTransform: (relativeSourcePath, _sourcemapPath) => {
|
||||||
|
return relativeSourcePath.replace(/^([^/]*\/){12}/, "./");
|
||||||
|
},
|
||||||
|
paths: pathsMap
|
||||||
|
}
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
typescript(),
|
||||||
|
postcss()
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,454 @@
|
||||||
|
/*
|
||||||
|
* VM-Operator
|
||||||
|
* Copyright (C) 2023 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
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jdrupes.vmoperator.vmviewer;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonGetter;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
|
import com.google.gson.JsonPrimitive;
|
||||||
|
import freemarker.core.ParseException;
|
||||||
|
import freemarker.template.MalformedTemplateNameException;
|
||||||
|
import freemarker.template.Template;
|
||||||
|
import freemarker.template.TemplateNotFoundException;
|
||||||
|
import io.kubernetes.client.util.Strings;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.Inet4Address;
|
||||||
|
import java.net.Inet6Address;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import org.jdrupes.json.JsonBeanDecoder;
|
||||||
|
import org.jdrupes.json.JsonDecodeException;
|
||||||
|
import org.jdrupes.vmoperator.common.K8sDynamicModel;
|
||||||
|
import org.jdrupes.vmoperator.common.K8sObserver;
|
||||||
|
import org.jdrupes.vmoperator.manager.events.ChannelCache;
|
||||||
|
import org.jdrupes.vmoperator.manager.events.GetDisplayPassword;
|
||||||
|
import org.jdrupes.vmoperator.manager.events.ModifyVm;
|
||||||
|
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||||
|
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||||
|
import org.jdrupes.vmoperator.util.GsonPtr;
|
||||||
|
import org.jgrapes.core.Channel;
|
||||||
|
import org.jgrapes.core.Event;
|
||||||
|
import org.jgrapes.core.Manager;
|
||||||
|
import org.jgrapes.core.annotation.Handler;
|
||||||
|
import org.jgrapes.http.Session;
|
||||||
|
import org.jgrapes.util.events.ConfigurationUpdate;
|
||||||
|
import org.jgrapes.util.events.KeyValueStoreQuery;
|
||||||
|
import org.jgrapes.util.events.KeyValueStoreUpdate;
|
||||||
|
import org.jgrapes.webconsole.base.Conlet.RenderMode;
|
||||||
|
import org.jgrapes.webconsole.base.ConletBaseModel;
|
||||||
|
import org.jgrapes.webconsole.base.ConsoleConnection;
|
||||||
|
import org.jgrapes.webconsole.base.ConsoleUser;
|
||||||
|
import org.jgrapes.webconsole.base.WebConsoleUtils;
|
||||||
|
import org.jgrapes.webconsole.base.events.AddConletType;
|
||||||
|
import org.jgrapes.webconsole.base.events.AddPageResources.ScriptResource;
|
||||||
|
import org.jgrapes.webconsole.base.events.ConletDeleted;
|
||||||
|
import org.jgrapes.webconsole.base.events.ConsoleReady;
|
||||||
|
import org.jgrapes.webconsole.base.events.DeleteConlet;
|
||||||
|
import org.jgrapes.webconsole.base.events.NotifyConletModel;
|
||||||
|
import org.jgrapes.webconsole.base.events.NotifyConletView;
|
||||||
|
import org.jgrapes.webconsole.base.events.OpenModalDialog;
|
||||||
|
import org.jgrapes.webconsole.base.events.RenderConlet;
|
||||||
|
import org.jgrapes.webconsole.base.events.RenderConletRequestBase;
|
||||||
|
import org.jgrapes.webconsole.base.events.SetLocale;
|
||||||
|
import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Class VmConlet.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports",
|
||||||
|
"PMD.CouplingBetweenObjects" })
|
||||||
|
public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
|
|
||||||
|
private static final Set<RenderMode> MODES = RenderMode.asSet(
|
||||||
|
RenderMode.Preview, RenderMode.Edit);
|
||||||
|
private final ChannelCache<String, VmChannel,
|
||||||
|
K8sDynamicModel> channelManager = new ChannelCache<>();
|
||||||
|
private static ObjectMapper objectMapper
|
||||||
|
= new ObjectMapper().registerModule(new JavaTimeModule());
|
||||||
|
private Class<?> preferredIpVersion = Inet4Address.class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The periodically generated update event.
|
||||||
|
*/
|
||||||
|
public static class Update extends Event<Void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new component with its channel set to the given channel.
|
||||||
|
*
|
||||||
|
* @param componentChannel the channel that the component's handlers listen
|
||||||
|
* on by default and that {@link Manager#fire(Event, Channel...)}
|
||||||
|
* sends the event to
|
||||||
|
*/
|
||||||
|
public VmViewer(Channel componentChannel) {
|
||||||
|
super(componentChannel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the component.
|
||||||
|
*
|
||||||
|
* @param event the event
|
||||||
|
*/
|
||||||
|
@Handler
|
||||||
|
public void onConfigurationUpdate(ConfigurationUpdate event) {
|
||||||
|
event.structured(componentPath()).ifPresent(c -> {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
var dispRes = (Map<String, Object>) c
|
||||||
|
.getOrDefault("displayResource", Collections.emptyMap());
|
||||||
|
switch ((String) dispRes.getOrDefault("preferredIpVersion", "")) {
|
||||||
|
case "ipv6":
|
||||||
|
preferredIpVersion = Inet6Address.class;
|
||||||
|
break;
|
||||||
|
case "ipv4":
|
||||||
|
default:
|
||||||
|
preferredIpVersion = Inet4Address.class;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On {@link ConsoleReady}, fire the {@link AddConletType}.
|
||||||
|
*
|
||||||
|
* @param event the event
|
||||||
|
* @param channel the channel
|
||||||
|
* @throws TemplateNotFoundException the template not found exception
|
||||||
|
* @throws MalformedTemplateNameException the malformed template name
|
||||||
|
* exception
|
||||||
|
* @throws ParseException the parse exception
|
||||||
|
* @throws IOException Signals that an I/O exception has occurred.
|
||||||
|
*/
|
||||||
|
@Handler
|
||||||
|
public void onConsoleReady(ConsoleReady event, ConsoleConnection channel)
|
||||||
|
throws TemplateNotFoundException, MalformedTemplateNameException,
|
||||||
|
ParseException, IOException {
|
||||||
|
// Add conlet resources to page
|
||||||
|
channel.respond(new AddConletType(type())
|
||||||
|
.setDisplayNames(
|
||||||
|
localizations(channel.supportedLocales(), "conletName"))
|
||||||
|
.addRenderMode(RenderMode.Preview)
|
||||||
|
.addScript(new ScriptResource().setScriptType("module")
|
||||||
|
.setScriptUri(event.renderSupport().conletResource(
|
||||||
|
type(), "VmViewer-functions.js"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String storagePath(Session session, String conletId) {
|
||||||
|
return "/" + WebConsoleUtils.userFromSession(session)
|
||||||
|
.map(ConsoleUser::getName).orElse("")
|
||||||
|
+ "/" + VmViewer.class.getName() + "/" + conletId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Optional<ViewerModel> createStateRepresentation(Event<?> event,
|
||||||
|
ConsoleConnection connection, String conletId) throws Exception {
|
||||||
|
var model = new ViewerModel(conletId);
|
||||||
|
String jsonState = objectMapper.writeValueAsString(model);
|
||||||
|
connection.respond(new KeyValueStoreUpdate().update(
|
||||||
|
storagePath(connection.session(), model.getConletId()), jsonState));
|
||||||
|
return Optional.of(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("PMD.EmptyCatchBlock")
|
||||||
|
protected Optional<ViewerModel> recreateState(Event<?> event,
|
||||||
|
ConsoleConnection channel, String conletId) throws Exception {
|
||||||
|
KeyValueStoreQuery query = new KeyValueStoreQuery(
|
||||||
|
storagePath(channel.session(), conletId), channel);
|
||||||
|
newEventPipeline().fire(query, channel);
|
||||||
|
try {
|
||||||
|
if (!query.results().isEmpty()) {
|
||||||
|
var json = query.results().get(0).values().stream().findFirst()
|
||||||
|
.get();
|
||||||
|
ViewerModel model
|
||||||
|
= objectMapper.readValue(json, ViewerModel.class);
|
||||||
|
return Optional.of(model);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// Means we have no result.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to creating default state.
|
||||||
|
return createStateRepresentation(event, channel, conletId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
|
||||||
|
protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event,
|
||||||
|
ConsoleConnection channel, String conletId, ViewerModel conletState)
|
||||||
|
throws Exception {
|
||||||
|
ResourceBundle resourceBundle = resourceBundle(channel.locale());
|
||||||
|
Set<RenderMode> renderedAs = new HashSet<>();
|
||||||
|
if (event.renderAs().contains(RenderMode.Preview)) {
|
||||||
|
Template tpl
|
||||||
|
= freemarkerConfig().getTemplate("VmViewer-preview.ftl.html");
|
||||||
|
channel.respond(new RenderConlet(type(), conletId,
|
||||||
|
processTemplate(event, tpl,
|
||||||
|
fmModel(event, channel, conletId, conletState)))
|
||||||
|
.setRenderAs(
|
||||||
|
RenderMode.Preview.addModifiers(event.renderAs()))
|
||||||
|
.setSupportedModes(MODES));
|
||||||
|
renderedAs.add(RenderMode.Preview);
|
||||||
|
if (!Strings.isNullOrEmpty(conletState.vmName())) {
|
||||||
|
updateConfig(channel, conletState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.renderAs().contains(RenderMode.Edit)) {
|
||||||
|
Template tpl = freemarkerConfig()
|
||||||
|
.getTemplate("VmViewer-edit.ftl.html");
|
||||||
|
var fmModel = fmModel(event, channel, conletId, conletState);
|
||||||
|
fmModel.put("vmNames",
|
||||||
|
channelManager.keys().stream().sorted().toList());
|
||||||
|
channel.respond(new OpenModalDialog(type(), conletId,
|
||||||
|
processTemplate(event, tpl, fmModel))
|
||||||
|
.addOption("cancelable", true)
|
||||||
|
.addOption("okayLabel",
|
||||||
|
resourceBundle.getString("okayLabel")));
|
||||||
|
}
|
||||||
|
return renderedAs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateConfig(ConsoleConnection channel, ViewerModel model) {
|
||||||
|
channel.respond(new NotifyConletView(type(),
|
||||||
|
model.getConletId(), "updateConfig", model.vmName()));
|
||||||
|
updateVmDef(channel, model);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateVmDef(ConsoleConnection channel, ViewerModel model) {
|
||||||
|
if (Strings.isNullOrEmpty(model.vmName())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
channelManager.associated(model.vmName()).ifPresent(vmDef -> {
|
||||||
|
try {
|
||||||
|
var def = JsonBeanDecoder.create(vmDef.data().toString())
|
||||||
|
.readObject();
|
||||||
|
channel.respond(new NotifyConletView(type(),
|
||||||
|
model.getConletId(), "updateVmDefinition", def));
|
||||||
|
} catch (JsonDecodeException e) {
|
||||||
|
logger.log(Level.SEVERE, e,
|
||||||
|
() -> "Failed to serialize VM definition");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doConletDeleted(ConletDeleted event,
|
||||||
|
ConsoleConnection channel, String conletId, ViewerModel conletState)
|
||||||
|
throws Exception {
|
||||||
|
if (event.renderModes().isEmpty()) {
|
||||||
|
channel.respond(new KeyValueStoreUpdate().delete(
|
||||||
|
storagePath(channel.session(), conletId)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track the VM definitions.
|
||||||
|
*
|
||||||
|
* @param event the event
|
||||||
|
* @param channel the channel
|
||||||
|
* @throws JsonDecodeException the json decode exception
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
@Handler(namedChannels = "manager")
|
||||||
|
@SuppressWarnings({ "PMD.ConfusingTernary", "PMD.CognitiveComplexity",
|
||||||
|
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals",
|
||||||
|
"PMD.ConfusingArgumentToVarargsMethod" })
|
||||||
|
public void onVmDefChanged(VmDefChanged event, VmChannel channel)
|
||||||
|
throws JsonDecodeException, IOException {
|
||||||
|
var vmDef = new K8sDynamicModel(channel.client().getJSON()
|
||||||
|
.getGson(), event.vmDefinition().data());
|
||||||
|
var vmName = vmDef.getMetadata().getName();
|
||||||
|
if (event.type() == K8sObserver.ResponseType.DELETED) {
|
||||||
|
channelManager.remove(vmName);
|
||||||
|
} else {
|
||||||
|
channelManager.put(vmName, channel, vmDef);
|
||||||
|
}
|
||||||
|
for (var entry : conletIdsByConsoleConnection().entrySet()) {
|
||||||
|
var connection = entry.getKey();
|
||||||
|
for (var conletId : entry.getValue()) {
|
||||||
|
var model = stateFromSession(connection.session(), conletId);
|
||||||
|
if (model.isEmpty() || !model.get().vmName().equals(vmName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (event.type() == K8sObserver.ResponseType.DELETED) {
|
||||||
|
connection.respond(
|
||||||
|
new DeleteConlet(conletId, Collections.emptySet()));
|
||||||
|
} else {
|
||||||
|
updateVmDef(connection, model.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings({ "PMD.AvoidDecimalLiteralsInBigDecimalConstructor",
|
||||||
|
"PMD.ConfusingArgumentToVarargsMethod" })
|
||||||
|
protected void doUpdateConletState(NotifyConletModel event,
|
||||||
|
ConsoleConnection channel, ViewerModel model)
|
||||||
|
throws Exception {
|
||||||
|
event.stop();
|
||||||
|
var vmName = event.params().asString(0);
|
||||||
|
var vmChannel = channelManager.channel(vmName).orElse(null);
|
||||||
|
if (vmChannel == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (event.method()) {
|
||||||
|
case "selectedVm":
|
||||||
|
model.setVmName(event.params().asString(0));
|
||||||
|
String jsonState = objectMapper.writeValueAsString(model);
|
||||||
|
channel.respond(new KeyValueStoreUpdate().update(storagePath(
|
||||||
|
channel.session(), model.getConletId()), jsonState));
|
||||||
|
updateConfig(channel, model);
|
||||||
|
break;
|
||||||
|
case "start":
|
||||||
|
fire(new ModifyVm(vmName, "state", "Running", vmChannel));
|
||||||
|
break;
|
||||||
|
case "stop":
|
||||||
|
fire(new ModifyVm(vmName, "state", "Stopped", vmChannel));
|
||||||
|
break;
|
||||||
|
case "openConsole":
|
||||||
|
channelManager.channel(vmName).ifPresent(
|
||||||
|
vc -> fire(Event.onCompletion(new GetDisplayPassword(vmName),
|
||||||
|
ds -> openConsole(vmName, channel, model, ds)), vc));
|
||||||
|
break;
|
||||||
|
default:// ignore
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openConsole(String vmName, ConsoleConnection connection,
|
||||||
|
ViewerModel model, GetDisplayPassword pwQuery) {
|
||||||
|
var vmDef = channelManager.associated(vmName).orElse(null);
|
||||||
|
if (vmDef == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var addr = displayIp(vmDef);
|
||||||
|
if (addr.isEmpty()) {
|
||||||
|
logger.severe(() -> "Failed to find display IP for " + vmName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var port = GsonPtr.to(vmDef.data()).get(JsonPrimitive.class, "spec",
|
||||||
|
"vm", "display", "spice", "port");
|
||||||
|
if (port.isEmpty()) {
|
||||||
|
logger.severe(() -> "No port defined for display of " + vmName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var proxyUrl = GsonPtr.to(vmDef.data()).get(JsonPrimitive.class, "spec",
|
||||||
|
"vm", "display", "spice", "proxyUrl");
|
||||||
|
StringBuffer data = new StringBuffer(100)
|
||||||
|
.append("[virt-viewer]\ntype=spice\nhost=")
|
||||||
|
.append(addr.get().getHostAddress()).append("\nport=")
|
||||||
|
.append(Integer.toString(port.get().getAsInt())).append('\n');
|
||||||
|
pwQuery.password().ifPresent(p -> {
|
||||||
|
data.append("password=").append(p).append('\n');
|
||||||
|
});
|
||||||
|
proxyUrl.map(JsonPrimitive::getAsString).ifPresent(u -> {
|
||||||
|
if (!Strings.isNullOrEmpty(u)) {
|
||||||
|
data.append("proxy=").append(u).append('\n');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connection.respond(new NotifyConletView(type(),
|
||||||
|
model.getConletId(), "openConsole", "application/x-virt-viewer",
|
||||||
|
Base64.getEncoder().encodeToString(data.toString().getBytes())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<InetAddress> displayIp(K8sDynamicModel vmDef) {
|
||||||
|
var server = GsonPtr.to(vmDef.data()).get(JsonPrimitive.class, "spec",
|
||||||
|
"vm", "display", "spice", "server");
|
||||||
|
if (server.isPresent()) {
|
||||||
|
var srv = server.get().getAsString();
|
||||||
|
try {
|
||||||
|
var addr = InetAddress.getByName(srv);
|
||||||
|
logger.fine(() -> "Using IP address from CRD for "
|
||||||
|
+ vmDef.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 = GsonPtr.to(vmDef.data()).getAsListOf(JsonPrimitive.class,
|
||||||
|
"nodeAddresses").stream().map(JsonPrimitive::getAsString)
|
||||||
|
.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 "
|
||||||
|
+ vmDef.getMetadata().getName() + ": " + addrs);
|
||||||
|
return addrs.stream()
|
||||||
|
.filter(a -> preferredIpVersion.isAssignableFrom(a.getClass()))
|
||||||
|
.findFirst().or(() -> addrs.stream().findFirst());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean doSetLocale(SetLocale event, ConsoleConnection channel,
|
||||||
|
String conletId) throws Exception {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Class VmsModel.
|
||||||
|
*/
|
||||||
|
public static class ViewerModel extends ConletBaseModel {
|
||||||
|
|
||||||
|
private String vmName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new vms model.
|
||||||
|
*
|
||||||
|
* @param conletId the conlet id
|
||||||
|
*/
|
||||||
|
public ViewerModel(@JsonProperty("conletId") String conletId) {
|
||||||
|
super(conletId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the vm name.
|
||||||
|
*
|
||||||
|
* @return the vmName
|
||||||
|
*/
|
||||||
|
@JsonGetter("vmName")
|
||||||
|
public String vmName() {
|
||||||
|
return vmName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the vm name.
|
||||||
|
*
|
||||||
|
* @param vmName the vmName to set
|
||||||
|
*/
|
||||||
|
public void setVmName(String vmName) {
|
||||||
|
this.vmName = vmName;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* VM-Operator
|
||||||
|
* Copyright (C) 2023 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
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jdrupes.vmoperator.vmviewer;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.jgrapes.core.Channel;
|
||||||
|
import org.jgrapes.core.ComponentType;
|
||||||
|
import org.jgrapes.webconsole.base.ConletComponentFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The factory service for {@link VmViewer}s.
|
||||||
|
*/
|
||||||
|
public class VmViewerFactory implements ConletComponentFactory {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* (non-Javadoc)
|
||||||
|
*
|
||||||
|
* @see org.jgrapes.core.ComponentFactory#componentType()
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Class<? extends ComponentType> componentType() {
|
||||||
|
return VmViewer.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* (non-Javadoc)
|
||||||
|
*
|
||||||
|
* @see org.jgrapes.core.ComponentFactory#create(org.jgrapes.core.Channel,
|
||||||
|
* java.util.Map)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Optional<ComponentType> create(Channel componentChannel,
|
||||||
|
Map<?, ?> properties) {
|
||||||
|
return Optional.of(new VmViewer(componentChannel));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,206 @@
|
||||||
|
/*
|
||||||
|
* VM-Operator
|
||||||
|
* Copyright (C) 2024 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
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
reactive, ref, createApp, computed, watch
|
||||||
|
} from "vue";
|
||||||
|
import JGConsole from "jgconsole";
|
||||||
|
import JgwcPlugin, { JGWC } from "jgwc";
|
||||||
|
import { provideApi, getApi } from "aash-plugin";
|
||||||
|
import l10nBundles from "l10nBundles";
|
||||||
|
|
||||||
|
import "./VmViewer-style.scss";
|
||||||
|
|
||||||
|
// For global access
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
orgJDrupesVmOperatorVmViewer: {
|
||||||
|
initPreview?: (previewDom: HTMLElement, isUpdate: boolean) => void,
|
||||||
|
initEdit?: (viewDom: HTMLElement, isUpdate: boolean) => void
|
||||||
|
applyEdit?: (viewDom: HTMLElement, apply: boolean) => void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.orgJDrupesVmOperatorVmViewer = {};
|
||||||
|
|
||||||
|
interface Api {
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
vmName: string;
|
||||||
|
vmDefinition: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localize = (key: string) => {
|
||||||
|
return JGConsole.localize(
|
||||||
|
l10nBundles, JGWC.lang(), key);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.orgJDrupesVmOperatorVmViewer.initPreview = (previewDom: HTMLElement,
|
||||||
|
_isUpdate: boolean) => {
|
||||||
|
const app = createApp({
|
||||||
|
setup(_props: object) {
|
||||||
|
const conletId = (<HTMLElement>previewDom.closest(
|
||||||
|
"[data-conlet-id]")!).dataset["conletId"]!;
|
||||||
|
const resourceBase = (<HTMLElement>previewDom.closest(
|
||||||
|
"*[data-conlet-resource-base]")!).dataset.conletResourceBase;
|
||||||
|
|
||||||
|
const previewApi: Api = reactive({
|
||||||
|
vmName: "",
|
||||||
|
vmDefinition: {}
|
||||||
|
});
|
||||||
|
const vmDef = computed(() => previewApi.vmDefinition);
|
||||||
|
|
||||||
|
watch(() => previewApi.vmName, (name: string) => {
|
||||||
|
if (name !== "") {
|
||||||
|
JGConsole.instance.updateConletTitle(conletId, name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
provideApi(previewDom, previewApi);
|
||||||
|
|
||||||
|
const vmAction = (vmName: string, action: string) => {
|
||||||
|
JGConsole.notifyConletModel(conletId, action, vmName);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { localize, resourceBase, vmDef, vmAction };
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><img role=button
|
||||||
|
v-on:click="vmAction(vmDef.name, 'openConsole')"
|
||||||
|
:src="resourceBase + (vmDef.running
|
||||||
|
? 'computer.svg' : 'computer-off.svg')"></td>
|
||||||
|
<td v-if="vmDef.spec"
|
||||||
|
class="jdrupes-vmoperator-vmviewer-preview-action-list">
|
||||||
|
<span role="button" v-if="vmDef.spec.vm.state != 'Running'"
|
||||||
|
tabindex="0" class="fa fa-play" :title="localize('Start VM')"
|
||||||
|
v-on:click="vmAction(vmDef.name, 'start')"></span>
|
||||||
|
<span role="button" v-else class="fa fa-play"
|
||||||
|
aria-disabled="true" :title="localize('Start VM')"></span>
|
||||||
|
<span role="button" v-if="vmDef.spec.vm.state != 'Stopped'"
|
||||||
|
tabindex="0" class="fa fa-stop" :title="localize('Stop VM')"
|
||||||
|
v-on:click="vmAction(vmDef.name, 'stop')"></span>
|
||||||
|
<span role="button" v-else class="fa fa-stop"
|
||||||
|
aria-disabled="true" :title="localize('Stop VM')"></span>
|
||||||
|
</td>
|
||||||
|
<td v-else>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>`
|
||||||
|
});
|
||||||
|
app.use(JgwcPlugin, []);
|
||||||
|
app.config.globalProperties.window = window;
|
||||||
|
app.mount(previewDom);
|
||||||
|
};
|
||||||
|
|
||||||
|
JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmviewer.VmViewer",
|
||||||
|
"updateConfig", function(conletId: string, vmName: string) {
|
||||||
|
const conlet = JGConsole.findConletPreview(conletId);
|
||||||
|
if (!conlet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const api = getApi<Api>(conlet.element().querySelector(
|
||||||
|
":scope .jdrupes-vmoperator-vmviewer-preview"))!;
|
||||||
|
api.vmName = vmName;
|
||||||
|
});
|
||||||
|
|
||||||
|
JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmviewer.VmViewer",
|
||||||
|
"updateVmDefinition", function(conletId: string, vmDefinition: any) {
|
||||||
|
const conlet = JGConsole.findConletPreview(conletId);
|
||||||
|
if (!conlet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const api = getApi<Api>(conlet.element().querySelector(
|
||||||
|
":scope .jdrupes-vmoperator-vmviewer-preview"))!;
|
||||||
|
// Add some short-cuts for rendering
|
||||||
|
vmDefinition.name = vmDefinition.metadata.name;
|
||||||
|
vmDefinition.currentCpus = vmDefinition.status.cpus;
|
||||||
|
vmDefinition.currentRam = Number(vmDefinition.status.ram);
|
||||||
|
for (const condition of vmDefinition.status.conditions) {
|
||||||
|
if (condition.type === "Running") {
|
||||||
|
vmDefinition.running = condition.status === "True";
|
||||||
|
vmDefinition.runningConditionSince
|
||||||
|
= new Date(condition.lastTransitionTime);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
api.vmDefinition = vmDefinition;
|
||||||
|
});
|
||||||
|
|
||||||
|
JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmviewer.VmViewer",
|
||||||
|
"openConsole", function(_conletId: string, mimeType: string, data: string) {
|
||||||
|
let target = document.getElementById(
|
||||||
|
"org.jdrupes.vmoperator.vmviewer.VmViewer.target");
|
||||||
|
if (!target) {
|
||||||
|
target = document.createElement("iframe");
|
||||||
|
target.id = "org.jdrupes.vmoperator.vmviewer.VmViewer.target";
|
||||||
|
target.setAttribute("name", target.id);
|
||||||
|
target.setAttribute("style", "display: none;");
|
||||||
|
document.querySelector("body")!.append(target);
|
||||||
|
}
|
||||||
|
const url = "data:" + mimeType + ";base64," + data;
|
||||||
|
window.open(url, target.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.orgJDrupesVmOperatorVmViewer.initEdit = (dialogDom: HTMLElement,
|
||||||
|
isUpdate: boolean) => {
|
||||||
|
if (isUpdate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const app = createApp({
|
||||||
|
setup() {
|
||||||
|
const formId = (<HTMLElement>dialogDom
|
||||||
|
.closest("*[data-conlet-id]")!).id + "-form";
|
||||||
|
|
||||||
|
const localize = (key: string) => {
|
||||||
|
return JGConsole.localize(
|
||||||
|
l10nBundles, JGWC.lang()!, key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const vmNameInput = ref<string>("");
|
||||||
|
const conletId = (<HTMLElement>dialogDom.closest(
|
||||||
|
"[data-conlet-id]")!).dataset["conletId"]!;
|
||||||
|
const conlet = JGConsole.findConletPreview(conletId);
|
||||||
|
if (conlet) {
|
||||||
|
const api = getApi<Api>(conlet.element().querySelector(
|
||||||
|
":scope .jdrupes-vmoperator-vmviewer-preview"))!;
|
||||||
|
vmNameInput.value = api.vmName;
|
||||||
|
}
|
||||||
|
|
||||||
|
provideApi(dialogDom, vmNameInput);
|
||||||
|
|
||||||
|
return { formId, localize, vmNameInput };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
app.use(JgwcPlugin);
|
||||||
|
app.mount(dialogDom);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.orgJDrupesVmOperatorVmViewer.applyEdit =
|
||||||
|
(dialogDom: HTMLElement, apply: boolean) => {
|
||||||
|
if (!apply) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const conletId = (<HTMLElement>dialogDom.closest("[data-conlet-id]")!)
|
||||||
|
.dataset["conletId"]!;
|
||||||
|
const vmName = getApi<ref<string>>(dialogDom!)!.value;
|
||||||
|
JGConsole.notifyConletModel(conletId, "selectedVm", vmName);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* VM-Operator
|
||||||
|
* Copyright (C) 2023 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
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Conlet specific styles.
|
||||||
|
*/
|
||||||
|
.jdrupes-vmoperator-vmviewer-preview img {
|
||||||
|
height: 3em;
|
||||||
|
padding: 0.25rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: var(--darkening);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.jdrupes-vmoperator-vmviewer-preview-action-list {
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
[role=button] {
|
||||||
|
padding: 0.25rem;
|
||||||
|
|
||||||
|
&:not([aria-disabled]):hover {
|
||||||
|
box-shadow: var(--darkening);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export default new Map<string, Map<string, string>>();
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* VM-Operator
|
||||||
|
* Copyright (C) 2023 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
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jdrupes.vmoperator.vmviewer;
|
||||||
23
org.jdrupes.vmoperator.vmviewer/tsconfig.json
Normal file
23
org.jdrupes.vmoperator.vmviewer/tsconfig.json
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2015",
|
||||||
|
"module": "es2015",
|
||||||
|
"sourceMap": true,
|
||||||
|
"inlineSources": true,
|
||||||
|
"declaration": true,
|
||||||
|
"importHelpers": true,
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"lib": ["DOM", "ES2020"],
|
||||||
|
"paths": {
|
||||||
|
"aash-plugin": ["./build/unpacked/org/jgrapes/webconsole/provider/jgwcvuecomponents/aash-vue-components/lib/AashPlugin"],
|
||||||
|
"jgconsole": ["./build/unpacked/org/jgrapes/webconsole/base/JGConsole"],
|
||||||
|
"jgwc": ["./build/unpacked/org/jgrapes/webconsole/provider/jgwcvuecomponents/jgwc-vue-components/jgwc-components"],
|
||||||
|
"l10nBundles": ["./src/org/jdrupes/vmoperator/vmviewer/browser/l10nBundles-stub"],
|
||||||
|
"vue": ["./build/unpacked/org/jgrapes/webconsole/provider/vue/vue/vue"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "l10nBundles-stub.ts"]
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ rootProject.name = 'VM-Operator'
|
||||||
include 'org.jdrupes.vmoperator.manager'
|
include 'org.jdrupes.vmoperator.manager'
|
||||||
include 'org.jdrupes.vmoperator.manager.events'
|
include 'org.jdrupes.vmoperator.manager.events'
|
||||||
include 'org.jdrupes.vmoperator.vmconlet'
|
include 'org.jdrupes.vmoperator.vmconlet'
|
||||||
|
include 'org.jdrupes.vmoperator.vmviewer'
|
||||||
include 'org.jdrupes.vmoperator.runner.qemu'
|
include 'org.jdrupes.vmoperator.runner.qemu'
|
||||||
include 'org.jdrupes.vmoperator.common'
|
include 'org.jdrupes.vmoperator.common'
|
||||||
include 'org.jdrupes.vmoperator.util'
|
include 'org.jdrupes.vmoperator.util'
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue