Implement basic reconciliation "loop".

This commit is contained in:
Michael Lipp 2023-07-22 14:36:42 +02:00
parent de17d323c3
commit 50bff5d38f
10 changed files with 461 additions and 68 deletions

2
.vscode/launch.json vendored
View file

@ -24,7 +24,7 @@
"mainClass": "org.jdrupes.vmoperator.runner.qemu.Runner",
"projectName": "org.jdrupes.vmoperator.runner.qemu",
"cwd": "${workspaceFolder}/org.jdrupes.vmoperator.runner.qemu",
"vmArgs": "-ea -Djava.util.logging.config.file=jul-debug.properties -Dorg.jdrupes.vmoperator.runner.qemu.config=./config.yaml"
"vmArgs": "-ea -Djava.util.logging.manager=org.jdrupes.vmoperator.util.LongLoggingManager"
}
]
}

View file

@ -17,6 +17,8 @@ spec:
type: object
properties:
image:
description: >-
The image to use for the pod. Must run a runner.
type: object
properties:
repository:
@ -34,25 +36,71 @@ spec:
default: "IfNotPresent"
vm:
type: object
description: Defines the VM.
properties:
name:
type: string
machineUuid:
description: >-
The machine's uuid. If none is specified, a uuid
is generated and stored in the data directory.
If the uuid is important (e.g. because licenses
depend on it) it is recommaned to specify it
explicitly or to carefully backup the data
directory.
type: string
host:
description: The host to run this vm on.
type: string
useTpm:
description: Whether to provide a software TPM.
type: boolean
default: false
firmware:
description: >-
How to boot.
type: string
enum: ["bios", "uefi", "uefi-4m", "secure", "secure-4m"]
default: "uefi"
bootMenu:
description: Whether to show a boot menu.
type: boolean
default: false
powerdownTimeout:
description: >-
When terminating, a graceful powerdown is attempted.
If it doesn't succeed within the given timeout
(seconds) SIGTERM is sent to Qemu.
type: integer
default: 900
cpuModel:
description: Any model supported by Qemu.
type: string
default: "host"
maximumCpus:
description: >-
Either maximumCpus or cpuTopology may be specified.
If currentCpus is greater than maximumCpus, the
latter is adjusted. Setting maximumCpus to 1 omits
the "-smp" options.
type: integer
default: 4
cpuTopology:
description: >-
The defaults (0) cause the corresponding property
to be omitted from the "-smp" option.
type: object
properties:
sockets:
type: integer
default: 0
diesPerSocket:
type: integer
default: 0
coresPerSocket:
type: integer
default: 0
threadsPerSocket:
type: integer
default: 0
currentCpus:
type: integer
default: 2
@ -62,25 +110,73 @@ spec:
currentRam:
type: string
rtcBase:
description: Passed to Qemu unmodified.
type: string
default: "utc"
spicePort:
type: integer
networks:
type: array
items:
description: >-
Supported types are "tap" and "user" (for debugging).
type: object
properties:
bridge:
tap:
type: object
properties:
name:
device:
description: The device to use.
type: string
default: "virtio-net"
bridge:
description: The bridge to attach to.
type: string
default: "br0"
mac:
type: string
required:
- name
user:
type: object
properties:
net:
type: string
oneOf:
- properties:
tap:
user:
disks:
description: >-
Disks make persistent storage available. The
storage may be provided by a device on the
host (preallocated, e.g. a LV).
type: array
items:
type: object
properties:
hostDevice:
type: string
bootindex:
type: integer
displays:
type: array
items:
type: object
properties:
spice:
type: object
properties:
port:
type: integer
default: 5900
ticket:
type: string
streamingVideo:
type: string
usbRedirects:
type: integer
default: 2
oneOf:
- properties:
maximumCpus:
cpuTopology:
required:
- vm
# either Namespaced or Cluster
@ -91,4 +187,5 @@ spec:
# singular name to be used as an alias on the CLI and for display
singular: vm
# kind is normally the CamelCased singular type. Your resource manifests use this.
kind: Vm
kind: VirtualMachine
listKind: VirtualMachineList

View file

@ -1,19 +1,24 @@
image:
repository: docker-registry.lan.mnl.de
path: vmoperator/org.jdrupes.vmoperator.runner.qemu-arch
pullPolicy: Always
apiVersion: "vmoperator.jdrupes.org/v1"
kind: VirtualMachine
metadata:
name: test-vm
spec:
image:
repository: docker-registry.lan.mnl.de
path: vmoperator/org.jdrupes.vmoperator.runner.qemu-arch
pullPolicy: Always
vm:
maximumCpus: 4
currentCpus: 4
maximumMemory: "8 GiB"
currentMemory: "4 GiB"
spicePort: 5910
vm:
maximumCpus: 4
currentCpus: 4
maximumRam: "8 GiB"
currentRam: "4 GiB"
# Currently only block devices are supported as VM disks
disks:
- device: /dev/vgmain/test-vm
size: 40Gi
networks:
- bridge:
mac: "00:16:3e:33:59:10"
networks:
- tap:
mac: "00:16:3e:33:59:10"
disks:
- hostDevice: /dev/vgmain/test-vm
displays:
- spice:
port: 5910

View file

@ -18,6 +18,8 @@ dependencies {
implementation 'commons-cli:commons-cli:1.5.0'
implementation 'io.kubernetes:client-java:18.0.0'
runtimeOnly 'org.slf4j:slf4j-jdk14:[2.0.7,3)'
}
application {

View file

@ -18,17 +18,6 @@
package org.jdrupes.vmoperator.manager;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.Configuration;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import io.kubernetes.client.openapi.apis.CustomObjectsApi;
import io.kubernetes.client.openapi.models.V1Pod;
import io.kubernetes.client.openapi.models.V1PodList;
import io.kubernetes.client.util.Config;
import io.kubernetes.client.util.Yaml;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
@ -38,48 +27,38 @@ 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.Start;
import org.jgrapes.core.events.Stop;
import org.jgrapes.io.NioDispatcher;
/**
* The application class.
*/
public class Manager extends Component {
/** The Constant APP_NAME. */
public static final String APP_NAME = "vmoperator";
private static Manager app;
/**
* Instantiates a new manager.
*
* @throws IOException Signals that an I/O exception has occurred.
*/
public Manager() throws IOException {
// Attach a general nio dispatcher
// Prepare component tree
attach(new NioDispatcher());
attach(new VmDefinitionWatcher(channel()));
attach(new Reconciliator(channel()));
}
/**
* Handle the start event.
* On stop.
*
* @param event the event
* @throws IOException
* @throws ApiException
*/
@Handler
public void onStart(Start event) throws IOException, ApiException {
ApiClient client = Config.defaultClient();
Configuration.setDefaultApiClient(client);
CoreV1Api api = new CoreV1Api();
V1PodList list = api.listPodForAllNamespaces(null, null, null, null,
null, null, null, null, null, null);
for (V1Pod item : list.getItems()) {
System.out.println(item.getMetadata().getName());
}
// CustomObjectsApi cApi = new CustomObjectsApi();
// var obj = cApi.getNamespacedCustomObject("vmoperator.jdrupes.org", "v1",
// "default", "vms", "test");
// obj = null;
}
@Handler
@Handler(priority = -1000)
public void onStop(Stop event) {
System.out.println("(Done.)");
logger.fine(() -> "Applictaion stopped.");
}
static {

View file

@ -0,0 +1,18 @@
package org.jdrupes.vmoperator.manager;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
import org.jgrapes.core.annotation.Handler;
public class Reconciliator extends Component {
public Reconciliator(Channel componentChannel) {
super(componentChannel);
}
@Handler
public void onVmChanged(VmChangedEvent event, WatchChannel channel) {
event = null;
}
}

View file

@ -0,0 +1,83 @@
/*
* JGrapes Event Driven Framework
* Copyright (C) 2018 Michael N. Lipp
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU 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 General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, see <http://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.manager;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Components;
import org.jgrapes.core.Event;
/**
* Indicates a change in a VM definition.
*/
public class VmChangedEvent extends Event<Void> {
/**
* The type of change.
*/
public enum Type {
ADDED, MODIFIED, DELETED
}
private final Type type;
private final V1ObjectMeta metadata;
/**
* Instantiates a new VM changed event.
*
* @param type the type
* @param metadata the metadata
*/
public VmChangedEvent(Type type, V1ObjectMeta metadata) {
this.type = type;
this.metadata = metadata;
}
/**
* Returns the type.
*
* @return the type
*/
public Type type() {
return type;
}
/**
* Returns the metadata.
*
* @return the metadata
*/
public V1ObjectMeta metadata() {
return metadata;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(Components.objectName(this)).append(" [").append(type)
.append(' ').append(metadata.getName());
if (channels() != null) {
builder.append(", channels=");
builder.append(Channel.toString(channels()));
}
builder.append(']');
return builder.toString();
}
}

View file

@ -0,0 +1,131 @@
/*
* JGrapes Event Driven Framework
* Copyright (C) 2018 Michael N. Lipp
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU 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 General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, see <http://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.manager;
import com.google.gson.reflect.TypeToken;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.Configuration;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import io.kubernetes.client.openapi.apis.CustomObjectsApi;
import io.kubernetes.client.openapi.models.V1APIResource;
import io.kubernetes.client.openapi.models.V1Namespace;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.util.Config;
import io.kubernetes.client.util.Watch;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import okhttp3.Call;
import org.jdrupes.vmoperator.manager.VmChangedEvent.Type;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
import org.jgrapes.core.annotation.Handler;
import org.jgrapes.core.events.Start;
import org.jgrapes.core.events.Stop;
/**
* Watches for changes of VM definitions.
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class VmDefinitionWatcher extends Component {
private static final String CR_GROUP = "vmoperator.jdrupes.org";
private static final String CR_VERSION = "v1";
private static final String CR_KIND = "VirtualMachine";
private CoreV1Api api;
private CustomObjectsApi coa;
private V1APIResource vmsCrd;
private String managedNamespace = "default";
private final Map<String, WatchChannel> channels
= new ConcurrentHashMap<>();
/**
* Instantiates a new VM definition watcher.
*
* @param componentChannel the component channel
*/
public VmDefinitionWatcher(Channel componentChannel) {
super(componentChannel);
}
/**
* Handle the start event.
*
* @param event the event
* @throws IOException
* @throws ApiException
*/
@Handler
public void onStart(Start event) throws IOException, ApiException {
ApiClient client = Config.defaultClient();
Configuration.setDefaultApiClient(client);
// Get access to APIs
api = new CoreV1Api();
coa = new CustomObjectsApi();
// Derive all information from the CRD
var resources = coa.getAPIResources(CR_GROUP, CR_VERSION);
vmsCrd = resources.getResources().stream()
.filter(r -> CR_KIND.equals(r.getKind())).findFirst().get();
// Watch the resources (vm definitions)
Call call = coa.listNamespacedCustomObjectCall(
CR_GROUP, CR_VERSION, managedNamespace, vmsCrd.getName(), null,
false, null, null, null, null, null, null, null, true, null);
new Thread(() -> {
try (Watch<V1Namespace> watch = Watch.createWatch(client,
call, new TypeToken<Watch.Response<V1Namespace>>() {
}.getType())) {
for (Watch.Response<V1Namespace> item : watch) {
handleCrEvent(item);
}
} catch (IOException | ApiException e) {
logger.log(Level.FINE, e, () -> "Probem while watching: "
+ e.getMessage());
}
fire(new Stop());
}).start();
}
private void handleCrEvent(Watch.Response<V1Namespace> item) {
V1ObjectMeta metadata = item.object.getMetadata();
WatchChannel channel = channels.computeIfAbsent(metadata.getName(),
k -> new WatchChannel(channel(), newEventPipeline(), api, coa));
channel.pipeline().fire(new VmChangedEvent(
VmChangedEvent.Type.valueOf(item.type), metadata), channel);
}
/**
* Remove VM channel when VM is deleted.
*
* @param event the event
* @param channel the channel
*/
@Handler(priority = -10_000)
public void onVmChanged(VmChangedEvent event, WatchChannel channel) {
if (event.type() == Type.DELETED) {
channels.remove(event.metadata().getName());
}
}
}

View file

@ -0,0 +1,78 @@
/*
* JGrapes Event Driven Framework
* Copyright (C) 2018 Michael N. Lipp
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU 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 General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, see <http://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.manager;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import io.kubernetes.client.openapi.apis.CustomObjectsApi;
import org.jgrapes.core.Channel;
import org.jgrapes.core.EventPipeline;
import org.jgrapes.core.Subchannel.DefaultSubchannel;
/**
* A subchannel used to send the events related to a specific
* VM.
*/
public class WatchChannel extends DefaultSubchannel {
private final EventPipeline pipeline;
private final CoreV1Api api;
private final CustomObjectsApi coa;
/**
* Instantiates a new watch channel.
*
* @param mainChannel the main channel
* @param pipeline the pipeline
*/
public WatchChannel(Channel mainChannel, EventPipeline pipeline,
CoreV1Api api, CustomObjectsApi coa) {
super(mainChannel);
this.pipeline = pipeline;
this.api = api;
this.coa = coa;
}
/**
* Returns the pipeline.
*
* @return the event pipeline
*/
public EventPipeline pipeline() {
return pipeline;
}
/**
* Returns the API object for invoking kubernetes functions.
*
* @return the API object
*/
public CoreV1Api api() {
return api;
}
/**
* Returns the API object for invoking kubernetes custom object
* functions.
*
* @return the API object
*/
public CustomObjectsApi coa() {
return coa;
}
}

View file

@ -1,5 +1,5 @@
#
#Tue Jun 13 14:18:19 CEST 2023
#Fri Jul 21 17:39:36 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.comment.indent_root_tags=false
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.enabling_tag=@formatter\:on
org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position=false
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.insert_new_line_after_annotation_on_parameter=do not insert
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.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.comment.preserve_white_space_between_code_and_line_comments=true
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_before_unary_operator=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.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