diff --git a/.project b/.project
index 505b6f1..e96d2da 100644
--- a/.project
+++ b/.project
@@ -2,37 +2,41 @@
VM-Operator
-
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.buildship.core.gradleprojectbuilder
+
+
+
+
+ net.sf.eclipsecs.core.CheckstyleBuilder
+
+
+
+
+ ch.acanda.eclipse.pmd.builder.PMDBuilder
+
+
+
+
org.eclipse.jdt.core.javanature
org.eclipse.buildship.core.gradleprojectnature
net.sf.eclipsecs.core.CheckstyleNature
ch.acanda.eclipse.pmd.builder.PMDNature
-
-
- org.eclipse.jdt.core.javabuilder
-
-
-
- org.eclipse.buildship.core.gradleprojectbuilder
-
-
-
- net.sf.eclipsecs.core.CheckstyleBuilder
-
-
-
- ch.acanda.eclipse.pmd.builder.PMDBuilder
-
-
-
-
1
+
30
-
org.eclipse.core.resources.regexFilterMatcher
node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__
diff --git a/.settings/org.eclipse.buildship.core.prefs b/.settings/org.eclipse.buildship.core.prefs
index 33d02f4..d0fed22 100644
--- a/.settings/org.eclipse.buildship.core.prefs
+++ b/.settings/org.eclipse.buildship.core.prefs
@@ -5,7 +5,7 @@ connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
connection.project.dir=
eclipse.preferences.version=1
gradle.user.home=
-java.home=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.362.b09-2.fc37.x86_64
+java.home=
jvm.arguments=
offline.mode=false
override.workspace.settings=true
diff --git a/buildSrc/.project b/buildSrc/.project
index 8624d9f..effb550 100644
--- a/buildSrc/.project
+++ b/buildSrc/.project
@@ -5,7 +5,20 @@
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.buildship.core.gradleprojectbuilder
+
+
+
+ org.eclipse.jdt.groovy.core.groovyNature
+ org.eclipse.jdt.core.javanature
+ org.eclipse.buildship.core.gradleprojectnature
diff --git a/buildSrc/.settings/org.eclipse.core.resources.prefs b/buildSrc/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..99f26c0
--- /dev/null
+++ b/buildSrc/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/=UTF-8
diff --git a/buildSrc/.settings/org.eclipse.core.runtime.prefs b/buildSrc/.settings/org.eclipse.core.runtime.prefs
new file mode 100644
index 0000000..5a0ad22
--- /dev/null
+++ b/buildSrc/.settings/org.eclipse.core.runtime.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+line.separator=\n
diff --git a/buildSrc/.settings/org.eclipse.jdt.core.prefs b/buildSrc/.settings/org.eclipse.jdt.core.prefs
index a15493a..68fda12 100644
--- a/buildSrc/.settings/org.eclipse.jdt.core.prefs
+++ b/buildSrc/.settings/org.eclipse.jdt.core.prefs
@@ -1,24 +1,22 @@
-#
-#Sun May 07 20:03:15 CEST 2023
-org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
-org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
-org.eclipse.jdt.core.compiler.problem.nullReference=warning
-org.eclipse.jdt.core.compiler.debug.localVariable=generate
-org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull
+org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=21
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=21
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
-org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
-org.eclipse.jdt.core.compiler.debug.sourceFile=generate
-org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
+org.eclipse.jdt.core.compiler.problem.nullReference=warning
org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore
-org.eclipse.jdt.core.compiler.source=17
-org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull
-org.eclipse.jdt.core.compiler.release=disabled
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
-org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
-org.eclipse.jdt.core.compiler.debug.lineNumber=generate
-org.eclipse.jdt.core.compiler.compliance=17
+org.eclipse.jdt.core.compiler.release=disabled
+org.eclipse.jdt.core.compiler.source=21
diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
index 8bad6d4..8a1e6e1 100644
--- a/buildSrc/build.gradle
+++ b/buildSrc/build.gradle
@@ -9,6 +9,9 @@ plugins {
// are build scripts in 'src/main' that automatically become available
// as plugins in the main build.
id 'groovy-gradle-plugin'
+
+ // Apply eclipse plugin
+ id 'eclipse'
}
repositories {
@@ -30,10 +33,35 @@ sourceSets {
}
}
-/*
-java {
- toolchain {
- languageVersion = JavaLanguageVersion.of(17)
+eclipse {
+
+ project {
+ file {
+ // closure executed after .project content is loaded from existing file
+ // and before gradle build information is merged
+ beforeMerged { project ->
+ project.natures.clear()
+ project.buildCommands.clear()
+ }
+
+ project.natures += 'org.eclipse.buildship.core.gradleprojectnature'
+ project.buildCommand 'org.eclipse.buildship.core.gradleprojectbuilder'
+ }
+ }
+
+ classpath {
+ downloadJavadoc = true
+ downloadSources = true
+ }
+
+ jdt {
+ file {
+ withProperties { properties ->
+ def formatterPrefs = new Properties()
+ rootProject.file("gradle/org.eclipse.jdt.core.formatter.prefs")
+ .withInputStream { formatterPrefs.load(it) }
+ properties.putAll(formatterPrefs)
+ }
+ }
}
}
-*/
diff --git a/deploy/crds/vms-crd.yaml b/deploy/crds/vms-crd.yaml
index 0b24bd6..713b785 100644
--- a/deploy/crds/vms-crd.yaml
+++ b/deploy/crds/vms-crd.yaml
@@ -9,6 +9,8 @@ spec:
- name: v1
served: true
storage: true
+ subresources:
+ status: {}
schema:
openAPIV3Schema:
type: object
@@ -1372,10 +1374,28 @@ spec:
- vm
status:
type: object
- properties:
+ default: {}
+ properties:
+ cpus:
+ description: >-
+ Number of CPUs currently in use.
+ type: integer
+ default: 0
+ ram:
+ description: >-
+ Amount of memory in use.
+ type: string
+ default: "0"
conditions:
description: >-
List of component conditions observed
+ default:
+ - type: Running
+ status: "False"
+ observedGeneration: 1
+ lastTransitionTime: "1970-01-01T00:00:00Z"
+ reason: Creation
+ message: "Creation of CR"
type: array
items:
type: object
@@ -1383,6 +1403,12 @@ spec:
Information about the condition of a component. See
https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
and https://github.com/kubernetes/apimachinery/blob/release-1.23/pkg/apis/meta/v1/types.go#L1432-L1492
+ required:
+ - type
+ - status
+ - lastTransitionTime
+ - reason
+ - message
properties:
type:
type: string
@@ -1428,12 +1454,6 @@ spec:
Message is a human readable message indicating
details about the transition. This may be an empty string.
default: ""
- required:
- - type
- - status
- - lastTransitionTime
- - reason
- - message
# either Namespaced or Cluster
scope: Namespaced
names:
diff --git a/deploy/kustomization.yaml b/deploy/kustomization.yaml
index 7193e0b..a988f88 100644
--- a/deploy/kustomization.yaml
+++ b/deploy/kustomization.yaml
@@ -7,4 +7,7 @@ resources:
- vmop-role-binding.yaml
- vmop-image-repository-pvc.yaml
- vmop-config-map.yaml
-- vmop-deployment.yaml
\ No newline at end of file
+- vmop-deployment.yaml
+- vmrunner-role.yaml
+- vmrunner-service-account.yaml
+- vmrunner-role-binding.yaml
diff --git a/deploy/vmrunner-role-binding.yaml b/deploy/vmrunner-role-binding.yaml
new file mode 100644
index 0000000..6e3f75b
--- /dev/null
+++ b/deploy/vmrunner-role-binding.yaml
@@ -0,0 +1,13 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: vm-runner
+ labels:
+ app.kubernetes.io/name: vm-operator
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: vm-runner
+subjects:
+- kind: ServiceAccount
+ name: vm-runner
diff --git a/deploy/vmrunner-role.yaml b/deploy/vmrunner-role.yaml
new file mode 100644
index 0000000..54e8742
--- /dev/null
+++ b/deploy/vmrunner-role.yaml
@@ -0,0 +1,20 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: vm-runner
+ labels:
+ app.kubernetes.io/name: vm-operator
+rules:
+- apiGroups:
+ - vmoperator.jdrupes.org
+ resources:
+ - vms
+ verbs:
+ - list
+ - get
+- apiGroups:
+ - vmoperator.jdrupes.org
+ resources:
+ - vms/status
+ verbs:
+ - patch
diff --git a/deploy/vmrunner-service-account.yaml b/deploy/vmrunner-service-account.yaml
new file mode 100644
index 0000000..c876dad
--- /dev/null
+++ b/deploy/vmrunner-service-account.yaml
@@ -0,0 +1,6 @@
+kind: ServiceAccount
+apiVersion: v1
+metadata:
+ name: vm-runner
+ labels:
+ app.kubernetes.io/name: vm-operator
diff --git a/org.jdrupes.vmoperator.manager/build.gradle b/org.jdrupes.vmoperator.manager/build.gradle
index 2c2dd7f..3a6dda3 100644
--- a/org.jdrupes.vmoperator.manager/build.gradle
+++ b/org.jdrupes.vmoperator.manager/build.gradle
@@ -16,8 +16,6 @@ dependencies {
implementation project(':org.jdrupes.vmoperator.util')
implementation 'commons-cli:commons-cli:1.5.0'
- implementation 'org.freemarker:freemarker:[2.3.32,2.4)'
- implementation 'io.kubernetes:client-java:[18.0.0,19)'
runtimeOnly 'com.electronwill.night-config:yaml:[3.6.7,3.7)'
runtimeOnly 'org.slf4j:slf4j-jdk14:[2.0.7,3)'
diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerSts.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerSts.ftl.yaml
index 60834b9..ac1178a 100644
--- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerSts.ftl.yaml
+++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerSts.ftl.yaml
@@ -140,6 +140,7 @@ spec:
<#if cr.spec.affinity??>
affinity: ${ cr.spec.affinity.toString() }
#if>
+ serviceAccountName: vm-runner
volumeClaimTemplates:
- metadata:
namespace: ${ cr.metadata.namespace.asString }
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java
index 340a3cd..15bdb76 100644
--- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java
@@ -32,6 +32,9 @@ import java.io.IOException;
import java.io.StringWriter;
import java.util.Map;
import java.util.logging.Logger;
+import static org.jdrupes.vmoperator.manager.Constants.APP_NAME;
+import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
+import org.jdrupes.vmoperator.util.K8s;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
@@ -98,8 +101,8 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
DynamicKubernetesObject newCm) {
ListOptions listOpts = new ListOptions();
listOpts.setLabelSelector(
- "app.kubernetes.io/managed-by=" + Constants.VM_OP_NAME + ","
- + "app.kubernetes.io/name=" + Constants.APP_NAME);
+ "app.kubernetes.io/managed-by=" + VM_OP_NAME + ","
+ + "app.kubernetes.io/name=" + APP_NAME);
// Get pod, selected by label
var podApi = new DynamicKubernetesApi("", "v1", "pods", client);
var pods = podApi
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Constants.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Constants.java
index d1482b6..fabae37 100644
--- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Constants.java
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Constants.java
@@ -21,16 +21,7 @@ package org.jdrupes.vmoperator.manager;
/**
* Some constants.
*/
-public class Constants {
-
- /** The Constant VM_OP_NAME. */
- public static final String VM_OP_NAME = "vm-operator";
-
- /** The Constant VM_OP_GROUP. */
- public static final String VM_OP_GROUP = "vmoperator.jdrupes.org";
-
- /** The Constant VM_OP_KIND_VM. */
- public static final String VM_OP_KIND_VM = "VirtualMachine";
+public class Constants extends org.jdrupes.vmoperator.util.Constants {
/** The Constant APP_NAME. */
public static final String APP_NAME = "vm-runner";
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java
index 998dc1d..28b078f 100644
--- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java
@@ -21,9 +21,11 @@ package org.jdrupes.vmoperator.manager;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.Configuration;
import java.io.IOException;
+import java.util.logging.Level;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
import org.jgrapes.core.annotation.Handler;
+import org.jgrapes.core.events.HandlingError;
import org.jgrapes.core.events.Start;
/**
@@ -74,6 +76,20 @@ public class Controller extends Component {
attach(new Reconciler(channel()));
}
+ /**
+ * Special handling of {@link ApiException} thrown by handlers.
+ *
+ * @param event the event
+ */
+ @Handler(channels = Channel.class)
+ public void onHandlingError(HandlingError event) {
+ if (event.throwable() instanceof ApiException exc) {
+ logger.log(Level.WARNING, exc,
+ () -> "Problem accessing kubernetes: " + exc.getResponseBody());
+ event.stop();
+ }
+ }
+
/**
* Handle the start event. Has higher priority because it configures
* the default Kubernetes client.
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/LoadBalancerReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/LoadBalancerReconciler.java
index a9c0d2b..b8decd8 100644
--- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/LoadBalancerReconciler.java
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/LoadBalancerReconciler.java
@@ -33,6 +33,8 @@ import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Logger;
+import org.jdrupes.vmoperator.util.GsonPtr;
+import org.jdrupes.vmoperator.util.K8s;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java
index 5b55ca8..a658846 100644
--- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java
@@ -32,9 +32,11 @@ import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
import org.jdrupes.vmoperator.util.FsdUtils;
+import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
import org.jgrapes.core.Components;
import org.jgrapes.core.annotation.Handler;
+import org.jgrapes.core.events.HandlingError;
import org.jgrapes.core.events.Stop;
import org.jgrapes.io.NioDispatcher;
import org.jgrapes.util.FileSystemWatcher;
@@ -95,6 +97,20 @@ public class Manager extends Component {
fire(new WatchFile(config.toPath()));
}
+ /**
+ * Log the exception when a handling error is reported.
+ *
+ * @param event the event
+ */
+ @Handler(channels = Channel.class, priority = -10_000)
+ @SuppressWarnings("PMD.GuardLogStatement")
+ public void onHandlingError(HandlingError event) {
+ logger.log(Level.WARNING, event.throwable(),
+ () -> "Problem invoking handler with " + event.event() + ": "
+ + event.message());
+ event.stop();
+ }
+
/**
* On stop.
*
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java
index 9777af1..ea43adf 100644
--- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java
@@ -47,6 +47,8 @@ import static org.jdrupes.vmoperator.manager.Constants.VM_OP_GROUP;
import org.jdrupes.vmoperator.manager.VmDefChanged.Type;
import org.jdrupes.vmoperator.util.Convertions;
import org.jdrupes.vmoperator.util.ExtendedObjectWrapper;
+import org.jdrupes.vmoperator.util.GsonPtr;
+import org.jdrupes.vmoperator.util.K8s;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
import org.jgrapes.core.annotation.Handler;
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/StatefuleSetReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/StatefuleSetReconciler.java
index 6f6d09c..94764ad 100644
--- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/StatefuleSetReconciler.java
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/StatefuleSetReconciler.java
@@ -29,6 +29,8 @@ import java.io.IOException;
import java.io.StringWriter;
import java.util.Map;
import java.util.logging.Logger;
+import org.jdrupes.vmoperator.util.GsonPtr;
+import org.jdrupes.vmoperator.util.K8s;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmWatcher.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmWatcher.java
index a304c0b..ab2dc4a 100644
--- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmWatcher.java
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmWatcher.java
@@ -44,8 +44,10 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
+import static org.jdrupes.vmoperator.manager.Constants.APP_NAME;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_GROUP;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_KIND_VM;
+import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
import org.jdrupes.vmoperator.manager.VmDefChanged.Type;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
@@ -132,7 +134,7 @@ public class VmWatcher extends Component {
for (var version : vmOpApiVersions) {
coa.getAPIResources(VM_OP_GROUP, version)
.getResources().stream()
- .filter(r -> Constants.VM_OP_KIND_VM.equals(r.getKind()))
+ .filter(r -> VM_OP_KIND_VM.equals(r.getKind()))
.findFirst()
.ifPresent(crd -> watchVmDefs(crd, version));
}
@@ -148,15 +150,15 @@ public class VmWatcher extends Component {
// Get all known CR instances.
coa.getAPIResources(VM_OP_GROUP, version)
.getResources().stream()
- .filter(r -> Constants.VM_OP_KIND_VM.equals(r.getKind()))
+ .filter(r -> VM_OP_KIND_VM.equals(r.getKind()))
.findFirst()
.ifPresent(crd -> known.addAll(getKnown(client, crd, version)));
}
ListOptions opts = new ListOptions();
opts.setLabelSelector(
- "app.kubernetes.io/managed-by=" + Constants.VM_OP_NAME + ","
- + "app.kubernetes.io/name=" + Constants.APP_NAME);
+ "app.kubernetes.io/managed-by=" + VM_OP_NAME + ","
+ + "app.kubernetes.io/name=" + APP_NAME);
for (String resource : List.of("apps/v1/statefulsets",
"v1/configmaps", "v1/secrets")) {
var resParts = new LinkedList<>(List.of(resource.split("/")));
diff --git a/org.jdrupes.vmoperator.runner.qemu/build.gradle b/org.jdrupes.vmoperator.runner.qemu/build.gradle
index 9f48a58..69f527f 100644
--- a/org.jdrupes.vmoperator.runner.qemu/build.gradle
+++ b/org.jdrupes.vmoperator.runner.qemu/build.gradle
@@ -16,8 +16,9 @@ dependencies {
implementation project(':org.jdrupes.vmoperator.util')
implementation 'commons-cli:commons-cli:1.5.0'
- implementation 'org.freemarker:freemarker:[2.3.32,2.4)'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:[2.15.1,3]'
+
+ runtimeOnly 'org.slf4j:slf4j-jdk14:[2.0.7,3)'
}
application {
diff --git a/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml b/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml
index 3e18871..461e79b 100644
--- a/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml
+++ b/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml
@@ -22,6 +22,11 @@
# the first time. Subsequent starts use the copy unless this option is set.
# "updateTemplate": false
+ # The namespace that this runner runs in. Usually obtained from
+ # /var/run/secrets/kubernetes.io/serviceaccount/namespace. Should only
+ # be set when starting the runner during development e.g. from the IDE.
+ # "namespace": ...
+
# Define the VM (required)
"vm":
# The VM's name (required)
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CpuController.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CpuController.java
index 6d8ccc1..f0face4 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CpuController.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CpuController.java
@@ -19,8 +19,8 @@
package org.jdrupes.vmoperator.runner.qemu;
import com.fasterxml.jackson.databind.node.ObjectNode;
-import java.util.ArrayList;
import java.util.HashSet;
+import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@@ -81,34 +81,28 @@ public class CpuController extends Component {
/**
* On monitor result.
*
- * @param result the result
+ * @param event the result
*/
@Handler
- public void onHotpluggableCpuStatus(HotpluggableCpuStatus result) {
- if (!result.successful()) {
+ public void onHotpluggableCpuStatus(HotpluggableCpuStatus event) {
+ if (!event.successful()) {
logger.warning(() -> "Failed to get hotpluggable CPU status "
- + "(won't adjust number of CPUs.): " + result.errorMessage());
+ + "(won't adjust number of CPUs.): " + event.errorMessage());
}
-
- // Sort
- List used = new ArrayList<>();
- List unused = new ArrayList<>();
- for (var itr = result.values().iterator(); itr.hasNext();) {
- ObjectNode cpu = (ObjectNode) itr.next();
- if (cpu.has("qom-path")) {
- used.add(cpu);
- } else {
- unused.add(cpu);
- }
- }
- currentCpus = used.size();
if (desiredCpus == null) {
return;
}
// Process
- int diff = used.size() - desiredCpus;
- diff = addCpus(used, unused, diff);
- deleteCpus(used, diff);
+ currentCpus = event.usedCpus().size();
+ int diff = currentCpus - desiredCpus;
+ if (diff == 0) {
+ return;
+ }
+ diff = addCpus(event.usedCpus(), event.unusedCpus(), diff);
+ removeCpus(event.usedCpus(), diff);
+
+ // Report result
+ fire(new MonitorCommand(new QmpQueryHotpluggableCpus()));
}
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
@@ -123,22 +117,24 @@ public class CpuController extends Component {
}
}
int nextId = 1;
- while (diff < 0 && !unused.isEmpty()) {
+ List remaining = new LinkedList<>(unused);
+ while (diff < 0 && !remaining.isEmpty()) {
String id;
do {
id = "cpu-" + nextId++;
} while (usedIds.contains(id));
- fire(new MonitorCommand(new QmpAddCpu(unused.get(0), id)));
- unused.remove(0);
+ fire(new MonitorCommand(new QmpAddCpu(remaining.get(0), id)));
+ remaining.remove(0);
diff += 1;
}
return diff;
}
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
- private int deleteCpus(List used, int diff) {
- while (diff > 0 && !used.isEmpty()) {
- ObjectNode cpu = used.remove(0);
+ private int removeCpus(List used, int diff) {
+ List removable = new LinkedList<>(used);
+ while (diff > 0 && !removable.isEmpty()) {
+ ObjectNode cpu = removable.remove(0);
String qomPath = cpu.get("qom-path").asText();
if (!qomPath.startsWith("/machine/peripheral/cpu-")) {
continue;
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java
index 1370e18..6ca3b2c 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java
@@ -58,14 +58,17 @@ import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State;
import org.jdrupes.vmoperator.util.ExtendedObjectWrapper;
import org.jdrupes.vmoperator.util.FsdUtils;
+import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
import org.jgrapes.core.Components;
import org.jgrapes.core.EventPipeline;
import org.jgrapes.core.TypedIdKey;
import org.jgrapes.core.annotation.Handler;
+import org.jgrapes.core.events.HandlingError;
import org.jgrapes.core.events.Start;
import org.jgrapes.core.events.Started;
import org.jgrapes.core.events.Stop;
+import org.jgrapes.core.internal.EventProcessor;
import org.jgrapes.io.NioDispatcher;
import org.jgrapes.io.events.Input;
import org.jgrapes.io.events.ProcessExited;
@@ -90,6 +93,13 @@ import org.jgrapes.util.events.WatchFile;
*
* 
*
+ * The {@link Runner} associates an {@link EventProcessor} with the
+ * {@link Start} event. This "runner event processor" must be used
+ * for all events related to the application level function. Components
+ * that handle events from other sources (and thus event processors)
+ * must fire any resulting events on the runner event processor in order
+ * to maintain synchronization.
+ *
* @startuml RunnerStates.svg
* [*] --> Initializing
* Initializing -> Initializing: InitialConfiguration/configure Runner
@@ -149,8 +159,13 @@ import org.jgrapes.util.events.WatchFile;
* error --> terminate
* StartingProcess --> terminate: ProcessExited
*
+ * state Stopped {
+ * state stopped <>
*
- * terminated --> [*]
+ * stopped --> [*]
+ * }
+ *
+ * terminated --> stopped
*
* @enduml
*
@@ -211,6 +226,7 @@ public class Runner extends Component {
attach(new ProcessManager(channel()));
attach(new SocketConnector(channel()));
attach(qemuMonitor = new QemuMonitor(channel()));
+ attach(new StatusUpdater(channel()));
// Configuration store with file in /etc/opt (default)
File config = new File(cmdLine.getOptionValue('c',
@@ -224,6 +240,20 @@ public class Runner extends Component {
fire(new WatchFile(config.toPath()));
}
+ /**
+ * Log the exception when a handling error is reported.
+ *
+ * @param event the event
+ */
+ @Handler(channels = Channel.class, priority = -10_000)
+ @SuppressWarnings("PMD.GuardLogStatement")
+ public void onHandlingError(HandlingError event) {
+ logger.log(Level.WARNING, event.throwable(),
+ () -> "Problem invoking handler with " + event.event() + ": "
+ + event.message());
+ event.stop();
+ }
+
/**
* On configuration update.
*
@@ -352,6 +382,11 @@ public class Runner extends Component {
return;
}
+ // Make sure to use thread specific client
+ // https://github.com/kubernetes-client/java/issues/100
+ io.kubernetes.client.openapi.Configuration.setDefaultApiClient(null);
+
+ // Prepare specific event pipeline to avoid concurrency.
rep = newEventPipeline();
event.setAssociated(EventPipeline.class, rep);
try {
@@ -387,7 +422,8 @@ public class Runner extends Component {
@Handler
public void onStarted(Started event) {
state = State.STARTING;
- fire(new RunnerStateChange(state));
+ rep.fire(new RunnerStateChange(state, "RunnerStarted",
+ "Runner has been started"));
// Start first process
if (config.vm.useTpm && swtpmDefinition != null) {
startProcess(swtpmDefinition);
@@ -476,7 +512,7 @@ public class Runner extends Component {
*/
@Handler
public void onMonitorReady(MonitorReady event) {
- fire(new RunnerConfigurationUpdate(config, state));
+ rep.fire(new RunnerConfigurationUpdate(config, state));
}
/**
@@ -489,7 +525,8 @@ public class Runner extends Component {
if (state == State.STARTING) {
fire(new MonitorCommand(new QmpCont()));
state = State.RUNNING;
- fire(new RunnerStateChange(state));
+ rep.fire(new RunnerStateChange(state, "VmStarted",
+ "Qemu has been configured and is continuing"));
}
}
@@ -524,9 +561,22 @@ public class Runner extends Component {
* @param event the event
*/
@Handler(priority = 10_000)
- public void onStop(Stop event) {
+ public void onStopFirst(Stop event) {
state = State.TERMINATING;
- fire(new RunnerStateChange(state));
+ rep.fire(new RunnerStateChange(state, "VmTerminating",
+ "The VM is being shut down"));
+ }
+
+ /**
+ * On stop.
+ *
+ * @param event the event
+ */
+ @Handler(priority = -10_000)
+ public void onStopLast(Stop event) {
+ state = State.STOPPED;
+ rep.fire(new RunnerStateChange(state, "VmStopped",
+ "The VM has been shut down"));
}
private void shutdown() {
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java
new file mode 100644
index 0000000..af8ab7a
--- /dev/null
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java
@@ -0,0 +1,304 @@
+/*
+ * 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 .
+ */
+
+package org.jdrupes.vmoperator.runner.qemu;
+
+import com.google.gson.JsonObject;
+import io.kubernetes.client.custom.Quantity;
+import io.kubernetes.client.custom.Quantity.Format;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.apis.ApisApi;
+import io.kubernetes.client.openapi.apis.CustomObjectsApi;
+import io.kubernetes.client.openapi.models.V1APIGroup;
+import io.kubernetes.client.openapi.models.V1GroupVersionForDiscovery;
+import io.kubernetes.client.util.Config;
+import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi;
+import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Instant;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.logging.Level;
+import org.jdrupes.vmoperator.runner.qemu.events.BalloonChangeEvent;
+import org.jdrupes.vmoperator.runner.qemu.events.HotpluggableCpuStatus;
+import org.jdrupes.vmoperator.runner.qemu.events.RunnerConfigurationUpdate;
+import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange;
+import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State;
+import static org.jdrupes.vmoperator.util.Constants.VM_OP_GROUP;
+import static org.jdrupes.vmoperator.util.Constants.VM_OP_KIND_VM;
+import org.jdrupes.vmoperator.util.GsonPtr;
+import org.jgrapes.core.Channel;
+import org.jgrapes.core.Component;
+import org.jgrapes.core.annotation.Handler;
+import org.jgrapes.core.events.HandlingError;
+import org.jgrapes.core.events.Start;
+import org.jgrapes.util.events.ConfigurationUpdate;
+import org.jgrapes.util.events.InitialConfiguration;
+
+/**
+ * Updates the CR status.
+ */
+public class StatusUpdater extends Component {
+
+ private static final Set RUNNING_STATES
+ = Set.of(State.RUNNING, State.TERMINATING);
+
+ private String namespace;
+ private String vmName;
+ private DynamicKubernetesApi vmCrApi;
+ private long observedGeneration;
+
+ /**
+ * Instantiates a new status updater.
+ *
+ * @param componentChannel the component channel
+ */
+ public StatusUpdater(Channel componentChannel) {
+ super(componentChannel);
+ }
+
+ /**
+ * On handling error.
+ *
+ * @param event the event
+ */
+ @Handler(channels = Channel.class)
+ public void onHandlingError(HandlingError event) {
+ if (event.throwable() instanceof ApiException exc) {
+ logger.log(Level.WARNING, exc,
+ () -> "Problem accessing kubernetes: " + exc.getResponseBody());
+ event.stop();
+ }
+ }
+
+ /**
+ * On configuration update.
+ *
+ * @param event the event
+ */
+ @Handler
+ @SuppressWarnings("unchecked")
+ public void onConfigurationUpdate(ConfigurationUpdate event) {
+ event.structured("/Runner").ifPresent(c -> {
+ if (event instanceof InitialConfiguration) {
+ namespace = (String) c.get("namespace");
+ updateNamespace();
+ vmName = Optional.ofNullable((Map) c.get("vm"))
+ .map(vm -> vm.get("name")).orElse(null);
+ }
+ });
+ }
+
+ private void updateNamespace() {
+ if (namespace == null) {
+ var path = Path
+ .of("/var/run/secrets/kubernetes.io/serviceaccount/namespace");
+ if (Files.isReadable(path)) {
+ try {
+ namespace = Files.lines(path).findFirst().orElse(null);
+ } catch (IOException e) {
+ logger.log(Level.WARNING, e,
+ () -> "Cannot read namespace.");
+ }
+ }
+ }
+ if (namespace == null) {
+ logger.warning(() -> "Namespace is unknown, some functions"
+ + " won't be available.");
+ }
+ }
+
+ /**
+ * Handle the start event.
+ *
+ * @param event the event
+ * @throws IOException
+ * @throws ApiException
+ */
+ @Handler
+ @SuppressWarnings({ "PMD.DataflowAnomalyAnalysis",
+ "PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals" })
+ public void onStart(Start event) throws IOException, ApiException {
+ var client = Config.defaultClient();
+ var apis = new ApisApi(client).getAPIVersions();
+ var crdVersions = apis.getGroups().stream()
+ .filter(g -> g.getName().equals(VM_OP_GROUP)).findFirst()
+ .map(V1APIGroup::getVersions).stream().flatMap(l -> l.stream())
+ .map(V1GroupVersionForDiscovery::getVersion).toList();
+ var coa = new CustomObjectsApi(client);
+ for (var crdVersion : crdVersions) {
+ var crdApiRes = coa.getAPIResources(VM_OP_GROUP,
+ crdVersion).getResources().stream()
+ .filter(r -> VM_OP_KIND_VM.equals(r.getKind())).findFirst();
+ if (crdApiRes.isEmpty()) {
+ continue;
+ }
+ var crApi = new DynamicKubernetesApi(VM_OP_GROUP,
+ crdVersion, crdApiRes.get().getName(), client);
+ var vmCr = crApi.get(namespace, vmName).throwsApiException();
+ if (vmCr.isSuccess()) {
+ vmCrApi = crApi;
+ observedGeneration
+ = vmCr.getObject().getMetadata().getGeneration();
+ break;
+ }
+ }
+ if (vmCrApi == null) {
+ logger.warning(() -> "Cannot find VM's CR, status will not"
+ + " be updated.");
+ }
+ }
+
+ @SuppressWarnings("PMD.AvoidDuplicateLiterals")
+ private JsonObject currentStatus(DynamicKubernetesObject vmCr) {
+ return vmCr.getRaw().getAsJsonObject("status").deepCopy();
+ }
+
+ /**
+ * On runner configuration update.
+ *
+ * @param event the event
+ * @throws ApiException
+ */
+ @Handler
+ public void onRunnerConfigurationUpdate(RunnerConfigurationUpdate event)
+ throws ApiException {
+ if (vmCrApi == null) {
+ return;
+ }
+ // A change of the runner configuration is typically caused
+ // by a new version of the CR. So we observe the new CR.
+ var vmCr = vmCrApi.get(namespace, vmName).throwsApiException()
+ .getObject();
+ if (vmCr.getMetadata().getGeneration() == observedGeneration) {
+ return;
+ }
+ vmCrApi.updateStatus(vmCr, from -> {
+ JsonObject status = currentStatus(from);
+ status.getAsJsonArray("conditions").asList().stream()
+ .map(cond -> (JsonObject) cond).filter(cond -> "Running"
+ .equals(cond.get("type").getAsString()))
+ .forEach(cond -> cond.addProperty("observedGeneration",
+ from.getMetadata().getGeneration()));
+ return status;
+ });
+ }
+
+ /**
+ * On runner state changed.
+ *
+ * @param event the event
+ * @throws ApiException
+ */
+ @Handler
+ public void onRunnerStateChanged(RunnerStateChange event)
+ throws ApiException {
+ if (vmCrApi == null) {
+ return;
+ }
+ var vmCr = vmCrApi.get(namespace, vmName).throwsApiException()
+ .getObject();
+ vmCrApi.updateStatus(vmCr, from -> {
+ JsonObject status = currentStatus(from);
+ status.getAsJsonArray("conditions").asList().stream()
+ .map(cond -> (JsonObject) cond)
+ .forEach(cond -> {
+ if ("Running".equals(cond.get("type").getAsString())) {
+ updateRunningCondition(event, from, cond);
+ }
+ });
+ if (event.state() == State.STARTING) {
+ status.addProperty("ram", GsonPtr.to(from.getRaw())
+ .getAsString("spec", "vm", "maximumRam").orElse("0"));
+ status.addProperty("cpus", 1);
+ } else if (event.state() == State.STOPPED) {
+ status.addProperty("ram", "0");
+ status.addProperty("cpus", 0);
+ }
+ return status;
+ }).throwsApiException();
+ }
+
+ private void updateRunningCondition(RunnerStateChange event,
+ DynamicKubernetesObject from, JsonObject cond) {
+ boolean reportedRunning
+ = "True".equals(cond.get("status").getAsString());
+ if (RUNNING_STATES.contains(event.state())
+ && !reportedRunning) {
+ cond.addProperty("status", "True");
+ cond.addProperty("lastTransitionTime",
+ Instant.now().toString());
+ }
+ if (!RUNNING_STATES.contains(event.state())
+ && reportedRunning) {
+ cond.addProperty("status", "False");
+ cond.addProperty("lastTransitionTime",
+ Instant.now().toString());
+ }
+ cond.addProperty("reason", event.reason());
+ cond.addProperty("message", event.message());
+ cond.addProperty("observedGeneration",
+ from.getMetadata().getGeneration());
+ }
+
+ /**
+ * On ballon change.
+ *
+ * @param event the event
+ * @throws ApiException
+ */
+ @Handler
+ public void onBallonChange(BalloonChangeEvent event) throws ApiException {
+ if (vmCrApi == null) {
+ return;
+ }
+ var vmCr = vmCrApi.get(namespace, vmName).throwsApiException()
+ .getObject();
+ vmCrApi.updateStatus(vmCr, from -> {
+ JsonObject status = currentStatus(from);
+ status.addProperty("ram",
+ new Quantity(new BigDecimal(event.size()), Format.BINARY_SI)
+ .toSuffixedString());
+ return status;
+ }).throwsApiException();
+ }
+
+ /**
+ * On ballon change.
+ *
+ * @param event the event
+ * @throws ApiException
+ */
+ @Handler
+ public void onCpuChange(HotpluggableCpuStatus event) throws ApiException {
+ if (vmCrApi == null) {
+ return;
+ }
+ var vmCr = vmCrApi.get(namespace, vmName).throwsApiException()
+ .getObject();
+ vmCrApi.updateStatus(vmCr, from -> {
+ JsonObject status = currentStatus(from);
+ status.addProperty("cpus", event.usedCpus().size());
+ return status;
+ }).throwsApiException();
+ }
+}
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/BalloonChangeEvent.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/BalloonChangeEvent.java
new file mode 100644
index 0000000..9cc67c8
--- /dev/null
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/BalloonChangeEvent.java
@@ -0,0 +1,47 @@
+/*
+ * 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 .
+ */
+
+package org.jdrupes.vmoperator.runner.qemu.events;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import java.math.BigInteger;
+
+/**
+ * Signals a change of the balloon.
+ */
+public class BalloonChangeEvent extends MonitorEvent {
+
+ /**
+ * Instantiates a new tray moved.
+ *
+ * @param kind the kind
+ * @param data the data
+ */
+ public BalloonChangeEvent(Kind kind, JsonNode data) {
+ super(kind, data);
+ }
+
+ /**
+ * Returns the actual value.
+ *
+ * @return the actual value
+ */
+ public BigInteger size() {
+ return new BigInteger(data().get("actual").asText());
+ }
+}
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/HotpluggableCpuStatus.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/HotpluggableCpuStatus.java
index 3ed2e50..68641c9 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/HotpluggableCpuStatus.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/HotpluggableCpuStatus.java
@@ -19,6 +19,10 @@
package org.jdrupes.vmoperator.runner.qemu.events;
import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
import org.jdrupes.vmoperator.runner.qemu.commands.QmpCommand;
/**
@@ -26,6 +30,9 @@ import org.jdrupes.vmoperator.runner.qemu.commands.QmpCommand;
*/
public class HotpluggableCpuStatus extends MonitorResult {
+ private List usedCpus = new ArrayList<>();
+ private List unusedCpus = new ArrayList<>();
+
/**
* Instantiates a new hotpluggable cpu result.
*
@@ -34,6 +41,39 @@ public class HotpluggableCpuStatus extends MonitorResult {
*/
public HotpluggableCpuStatus(QmpCommand command, JsonNode response) {
super(command, response);
+ if (!successful()) {
+ return;
+ }
+
+ // Sort
+ for (var itr = values().iterator(); itr.hasNext();) {
+ ObjectNode cpu = (ObjectNode) itr.next();
+ if (cpu.has("qom-path")) {
+ usedCpus.add(cpu);
+ } else {
+ unusedCpus.add(cpu);
+ }
+ }
+ usedCpus = Collections.unmodifiableList(usedCpus);
+ unusedCpus = Collections.unmodifiableList(unusedCpus);
+ }
+
+ /**
+ * Gets the used cpus.
+ *
+ * @return the usedCpus
+ */
+ public List usedCpus() {
+ return usedCpus;
+ }
+
+ /**
+ * Gets the unused cpus.
+ *
+ * @return the unusedCpus
+ */
+ public List unusedCpus() {
+ return unusedCpus;
}
}
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorEvent.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorEvent.java
index 28d2e4c..72647a1 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorEvent.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorEvent.java
@@ -32,7 +32,7 @@ public class MonitorEvent extends Event {
* The kind of monitor event.
*/
public enum Kind {
- READY, POWERDOWN, DEVICE_TRAY_MOVED
+ READY, POWERDOWN, DEVICE_TRAY_MOVED, BALLOON_CHANGE
}
private final Kind kind;
@@ -55,6 +55,9 @@ public class MonitorEvent extends Event {
case DEVICE_TRAY_MOVED:
return Optional
.of(new TrayMovedEvent(kind, response.get("data")));
+ case BALLOON_CHANGE:
+ return Optional
+ .of(new BalloonChangeEvent(kind, response.get("data")));
default:
return Optional
.of(new MonitorEvent(kind, response.get("data")));
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerStateChange.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerStateChange.java
index a827fa4..46fa1f8 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerStateChange.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerStateChange.java
@@ -19,6 +19,7 @@
package org.jdrupes.vmoperator.runner.qemu.events;
import org.jgrapes.core.Channel;
+import org.jgrapes.core.Components;
import org.jgrapes.core.Event;
/**
@@ -30,19 +31,24 @@ public class RunnerStateChange extends Event {
* The state.
*/
public enum State {
- INITIALIZING, STARTING, RUNNING, TERMINATING
+ INITIALIZING, STARTING, RUNNING, TERMINATING, STOPPED
}
private final State state;
+ private final String reason;
+ private final String message;
/**
* Instantiates a new runner state change.
*
* @param channels the channels
*/
- public RunnerStateChange(State state, Channel... channels) {
+ public RunnerStateChange(State state, String reason, String message,
+ Channel... channels) {
super(channels);
this.state = state;
+ this.reason = reason;
+ this.message = message;
}
/**
@@ -53,4 +59,36 @@ public class RunnerStateChange extends Event {
public State state() {
return state;
}
+
+ /**
+ * Gets the reason.
+ *
+ * @return the reason
+ */
+ public String reason() {
+ return reason;
+ }
+
+ /**
+ * Gets the message.
+ *
+ * @return the message
+ */
+ public String message() {
+ return message;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append(Components.objectName(this))
+ .append(" [").append(state).append(": ").append(reason);
+ if (channels() != null) {
+ builder.append(", channels=");
+ builder.append(Channel.toString(channels()));
+ }
+ builder.append(']');
+ return builder.toString();
+ }
+
}
diff --git a/org.jdrupes.vmoperator.util/.settings/org.eclipse.jdt.core.prefs b/org.jdrupes.vmoperator.util/.settings/org.eclipse.jdt.core.prefs
index 4250f30..672f85e 100644
--- a/org.jdrupes.vmoperator.util/.settings/org.eclipse.jdt.core.prefs
+++ b/org.jdrupes.vmoperator.util/.settings/org.eclipse.jdt.core.prefs
@@ -1,5 +1,5 @@
#
-#Fri Sep 01 16:56:14 CEST 2023
+#Thu Sep 14 22:05:28 CEST 2023
org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert
org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert
@@ -22,12 +22,12 @@ org.eclipse.jdt.core.formatter.blank_lines_after_package=1
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
-org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true
org.eclipse.jdt.core.formatter.comment.indent_root_tags=false
+org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true
org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on
org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position=false
-org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert
org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert
@@ -43,8 +43,8 @@ org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invoc
org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert
-org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=true
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert
org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16
org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0
@@ -63,8 +63,8 @@ org.eclipse.jdt.core.formatter.alignment_for_type_parameters=16
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false
org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=false
org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation=common_lines
org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert
diff --git a/org.jdrupes.vmoperator.util/build.gradle b/org.jdrupes.vmoperator.util/build.gradle
index 0dac0c5..3c772a8 100644
--- a/org.jdrupes.vmoperator.util/build.gradle
+++ b/org.jdrupes.vmoperator.util/build.gradle
@@ -9,5 +9,6 @@ plugins {
}
dependencies {
- implementation 'org.freemarker:freemarker:[2.3.32,2.4)'
+ api 'org.freemarker:freemarker:[2.3.32,2.4)'
+ api 'io.kubernetes:client-java:[18.0.0,19)'
}
diff --git a/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/Constants.java b/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/Constants.java
new file mode 100644
index 0000000..ba710d1
--- /dev/null
+++ b/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/Constants.java
@@ -0,0 +1,34 @@
+/*
+ * 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 .
+ */
+
+package org.jdrupes.vmoperator.util;
+
+/**
+ * Some constants.
+ */
+public class Constants {
+
+ /** The Constant VM_OP_NAME. */
+ public static final String VM_OP_NAME = "vm-operator";
+
+ /** The Constant VM_OP_GROUP. */
+ public static final String VM_OP_GROUP = "vmoperator.jdrupes.org";
+
+ /** The Constant VM_OP_KIND_VM. */
+ public static final String VM_OP_KIND_VM = "VirtualMachine";
+}
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/GsonPtr.java b/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/GsonPtr.java
similarity index 99%
rename from org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/GsonPtr.java
rename to org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/GsonPtr.java
index 7d0f403..22a76f3 100644
--- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/GsonPtr.java
+++ b/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/GsonPtr.java
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-package org.jdrupes.vmoperator.manager;
+package org.jdrupes.vmoperator.util;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/K8s.java b/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/K8s.java
similarity index 99%
rename from org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/K8s.java
rename to org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/K8s.java
index efc304e..539a870 100644
--- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/K8s.java
+++ b/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/K8s.java
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-package org.jdrupes.vmoperator.manager;
+package org.jdrupes.vmoperator.util;
import io.kubernetes.client.common.KubernetesListObject;
import io.kubernetes.client.common.KubernetesObject;