Feature/status (#5)
Update CR's status (condition Running, current cpu and ram allocation).
This commit is contained in:
commit
d8882359a6
36 changed files with 776 additions and 115 deletions
46
.project
46
.project
|
|
@ -2,37 +2,41 @@
|
|||
<projectDescription>
|
||||
<name>VM-Operator</name>
|
||||
<comment></comment>
|
||||
<projects/>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>net.sf.eclipsecs.core.CheckstyleBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>ch.acanda.eclipse.pmd.builder.PMDBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
|
||||
<nature>net.sf.eclipsecs.core.CheckstyleNature</nature>
|
||||
<nature>ch.acanda.eclipse.pmd.builder.PMDNature</nature>
|
||||
</natures>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments/>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
|
||||
<arguments/>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>net.sf.eclipsecs.core.CheckstyleBuilder</name>
|
||||
<arguments/>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>ch.acanda.eclipse.pmd.builder.PMDBuilder</name>
|
||||
<arguments/>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<linkedResources/>
|
||||
<filteredResources>
|
||||
<filter>
|
||||
<id>1</id>
|
||||
<name></name>
|
||||
<type>30</type>
|
||||
<name/>
|
||||
<matcher>
|
||||
<id>org.eclipse.core.resources.regexFilterMatcher</id>
|
||||
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -5,7 +5,20 @@
|
|||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.groovy.core.groovyNature</nature>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
|
|
|
|||
2
buildSrc/.settings/org.eclipse.core.resources.prefs
Normal file
2
buildSrc/.settings/org.eclipse.core.resources.prefs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
eclipse.preferences.version=1
|
||||
encoding/<project>=UTF-8
|
||||
2
buildSrc/.settings/org.eclipse.core.runtime.prefs
Normal file
2
buildSrc/.settings/org.eclipse.core.runtime.prefs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
eclipse.preferences.version=1
|
||||
line.separator=\n
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -8,3 +8,6 @@ resources:
|
|||
- vmop-image-repository-pvc.yaml
|
||||
- vmop-config-map.yaml
|
||||
- vmop-deployment.yaml
|
||||
- vmrunner-role.yaml
|
||||
- vmrunner-service-account.yaml
|
||||
- vmrunner-role-binding.yaml
|
||||
|
|
|
|||
13
deploy/vmrunner-role-binding.yaml
Normal file
13
deploy/vmrunner-role-binding.yaml
Normal file
|
|
@ -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
|
||||
20
deploy/vmrunner-role.yaml
Normal file
20
deploy/vmrunner-role.yaml
Normal file
|
|
@ -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
|
||||
6
deploy/vmrunner-service-account.yaml
Normal file
6
deploy/vmrunner-service-account.yaml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
kind: ServiceAccount
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: vm-runner
|
||||
labels:
|
||||
app.kubernetes.io/name: vm-operator
|
||||
|
|
@ -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)'
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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("/")));
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<ObjectNode> used = new ArrayList<>();
|
||||
List<ObjectNode> 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<ObjectNode> 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<ObjectNode> used, int diff) {
|
||||
while (diff > 0 && !used.isEmpty()) {
|
||||
ObjectNode cpu = used.remove(0);
|
||||
private int removeCpus(List<ObjectNode> used, int diff) {
|
||||
List<ObjectNode> 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;
|
||||
|
|
|
|||
|
|
@ -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 <<entryPoint>>
|
||||
*
|
||||
* 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() {
|
||||
|
|
|
|||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<State> 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<String, String>) 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ObjectNode> usedCpus = new ArrayList<>();
|
||||
private List<ObjectNode> 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<ObjectNode> usedCpus() {
|
||||
return usedCpus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the unused cpus.
|
||||
*
|
||||
* @return the unusedCpus
|
||||
*/
|
||||
public List<ObjectNode> unusedCpus() {
|
||||
return unusedCpus;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ public class MonitorEvent extends Event<Void> {
|
|||
* 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<Void> {
|
|||
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")));
|
||||
|
|
|
|||
|
|
@ -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<Void> {
|
|||
* 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<Void> {
|
|||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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";
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.jdrupes.vmoperator.manager;
|
||||
package org.jdrupes.vmoperator.util;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.jdrupes.vmoperator.manager;
|
||||
package org.jdrupes.vmoperator.util;
|
||||
|
||||
import io.kubernetes.client.common.KubernetesListObject;
|
||||
import io.kubernetes.client.common.KubernetesObject;
|
||||
Loading…
Add table
Add a link
Reference in a new issue