From 50bff5d38fc0a21719b87f679159eb207dbb8f37 Mon Sep 17 00:00:00 2001 From: "Michael N. Lipp" Date: Sat, 22 Jul 2023 14:36:42 +0200 Subject: [PATCH] Implement basic reconciliation "loop". --- .vscode/launch.json | 2 +- deploy/crds/vmoperator-crd.yaml | 115 +++++++++++++-- deploy/test-vm.yaml | 39 +++--- org.jdrupes.vmoperator.manager/build.gradle | 2 + .../jdrupes/vmoperator/manager/Manager.java | 51 ++----- .../vmoperator/manager/Reconciliator.java | 18 +++ .../vmoperator/manager/VmChangedEvent.java | 83 +++++++++++ .../manager/VmDefinitionWatcher.java | 131 ++++++++++++++++++ .../vmoperator/manager/WatchChannel.java | 78 +++++++++++ .../.settings/org.eclipse.jdt.core.prefs | 10 +- 10 files changed, 461 insertions(+), 68 deletions(-) create mode 100644 org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciliator.java create mode 100644 org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmChangedEvent.java create mode 100644 org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmDefinitionWatcher.java create mode 100644 org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/WatchChannel.java diff --git a/.vscode/launch.json b/.vscode/launch.json index b6d2ca5..070f376 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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" } ] } \ No newline at end of file diff --git a/deploy/crds/vmoperator-crd.yaml b/deploy/crds/vmoperator-crd.yaml index 134845f..b5792b9 100644 --- a/deploy/crds/vmoperator-crd.yaml +++ b/deploy/crds/vmoperator-crd.yaml @@ -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 diff --git a/deploy/test-vm.yaml b/deploy/test-vm.yaml index c4fa347..e964360 100644 --- a/deploy/test-vm.yaml +++ b/deploy/test-vm.yaml @@ -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 diff --git a/org.jdrupes.vmoperator.manager/build.gradle b/org.jdrupes.vmoperator.manager/build.gradle index e950486..7a96ae3 100644 --- a/org.jdrupes.vmoperator.manager/build.gradle +++ b/org.jdrupes.vmoperator.manager/build.gradle @@ -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 { 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 7b074fd..2e58469 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 @@ -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 { diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciliator.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciliator.java new file mode 100644 index 0000000..6d9b3e1 --- /dev/null +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciliator.java @@ -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; + } + +} diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmChangedEvent.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmChangedEvent.java new file mode 100644 index 0000000..507916d --- /dev/null +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmChangedEvent.java @@ -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 . + */ + +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 { + + /** + * 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(); + } +} diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmDefinitionWatcher.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmDefinitionWatcher.java new file mode 100644 index 0000000..1260800 --- /dev/null +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmDefinitionWatcher.java @@ -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 . + */ + +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 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 watch = Watch.createWatch(client, + call, new TypeToken>() { + }.getType())) { + for (Watch.Response 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 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()); + } + } + +} diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/WatchChannel.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/WatchChannel.java new file mode 100644 index 0000000..c5d1924 --- /dev/null +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/WatchChannel.java @@ -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 . + */ + +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; + } +} 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 4748889..0dab961 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 @@ # -#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