diff --git a/deploy/crds/vmops-config-crd.yaml b/deploy/crds/vmops-config-crd.yaml new file mode 100644 index 0000000..61f09f5 --- /dev/null +++ b/deploy/crds/vmops-config-crd.yaml @@ -0,0 +1,32 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: vmopconfigs.vmoperator.jdrupes.org +spec: + group: vmoperator.jdrupes.org + # list of versions supported by this CustomResourceDefinition + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + imageRepository: + type: object + description: Defines the image repository volume. + properties: {} + # either Namespaced or Cluster + scope: Namespaced + names: + # plural name to be used in the URL: /apis/// + plural: vmopconfigs + # singular name to be used as an alias on the CLI and for display + singular: vmopconfig + # kind is normally the CamelCased singular type. Your resource manifests use this. + kind: VmOpConfig + listKind: VmOpConfigList diff --git a/deploy/crds/vms-crd.yaml b/deploy/crds/vms-crd.yaml index c1950f0..2e2d98f 100644 --- a/deploy/crds/vms-crd.yaml +++ b/deploy/crds/vms-crd.yaml @@ -431,10 +431,24 @@ spec: type: string type: object type: object + cdromImage: + type: object + properties: + path: + type: string + required: + - path bootindex: type: integer - required: - - volumeClaimTemplate + oneOf: + - properties: + volumeClaimTemplate: + required: + - volumeClaimTemplate + - properties: + cdromImage: + required: + - cdromImage default: [] display: type: object diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml index ef1b578..0104211 100644 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml +++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml @@ -136,15 +136,28 @@ data: drives: <#assign drvCounter = 0/> <#list cr.spec.vm.disks.asList() as disk> - <#if disk.volumeClaimTemplate.metadata?? + <#if disk.volumeClaimTemplate?? + && disk.volumeClaimTemplate.metadata?? && disk.volumeClaimTemplate.metadata.name??> <#assign name = disk.volumeClaimTemplate.metadata.name.asString> <#else> <#assign name = "" + drvCounter> + <#if disk.volumeClaimTemplate??> - type: raw resource: /dev/disk-${ name } - <#assign drvCounter = drvCounter + 1/> + <#if disk.bootindex??> + bootindex: ${ disk.bootindex.asInt?c } + + <#assign drvCounter = drvCounter + 1/> + + <#if disk.cdromImage??> + - type: ide-cd + file: "${ disk.cdromImage.path.asString }" + <#if disk.bootindex??> + bootindex: ${ disk.bootindex.asInt?c } + + display: diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerPod.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerPod.ftl.yaml index f05c0fb..905e53c 100644 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerPod.ftl.yaml +++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerPod.ftl.yaml @@ -36,6 +36,7 @@ spec: volumeDevices: <#assign diskCounter = 0/> <#list cr.spec.vm.disks.asList() as disk> + <#if disk.volumeClaimTemplate??> <#if disk.volumeClaimTemplate.metadata?? && disk.volumeClaimTemplate.metadata.name??> <#assign diskName = "disk-" + disk.volumeClaimTemplate.metadata.name.asString> @@ -45,6 +46,7 @@ spec: - name: ${ diskName } devicePath: /dev/${ diskName } <#assign diskCounter = diskCounter + 1/> + securityContext: privileged: true @@ -72,6 +74,7 @@ spec: claimName: vmop-image-repository <#assign diskCounter = 0/> <#list cr.spec.vm.disks.asList() as disk> + <#if disk.volumeClaimTemplate??> <#if disk.volumeClaimTemplate.metadata?? && disk.volumeClaimTemplate.metadata.name??> <#assign claimName = disk.volumeClaimTemplate.metadata.name.asString> @@ -84,6 +87,7 @@ spec: persistentVolumeClaim: claimName: ${ claimName } <#assign diskCounter = diskCounter + 1/> + hostNetwork: true terminationGracePeriodSeconds: ${ (cr.spec.vm.powerdownTimeout.asInt + 5)?c } diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisksReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisksReconciler.java index a65bfcd..6c4bdbe 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisksReconciler.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisksReconciler.java @@ -58,6 +58,9 @@ import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME; private void reconcileDisk(DynamicKubernetesObject vmDefinition, int index, JsonObject diskDef, WatchChannel channel) throws ApiException { + if (!diskDef.has("volumeClaimTemplate")) { + return; + } var pvcObject = new DynamicKubernetesObject(); var pvcRaw = GsonPtr.to(pvcObject.getRaw()); var vmRaw = GsonPtr.to(vmDefinition.getRaw()); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CdromController.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CdromController.java new file mode 100644 index 0000000..1ab44da --- /dev/null +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CdromController.java @@ -0,0 +1,104 @@ +/* + * VM-Operator + * Copyright (C) 2023 Michael N. Lipp + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jdrupes.vmoperator.runner.qemu; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.io.IOException; +import java.util.logging.Level; +import org.jdrupes.vmoperator.runner.qemu.events.ChangeMediumCommand; +import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand.Command; +import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommandCompleted; +import org.jgrapes.core.Channel; +import org.jgrapes.core.Component; +import org.jgrapes.core.annotation.Handler; + +// TODO: Auto-generated Javadoc +/** + * The Class CdromController. + */ +public class CdromController extends Component { + + private static ObjectMapper mapper; + private static JsonNode openTray; + private static JsonNode removeMedium; + private static JsonNode changeMedium; + private final QemuMonitor monitor; + + /** + * Instantiates a new cdrom controller. + * + * @param componentChannel the component channel + * @param monitor the monitor + */ + @SuppressWarnings("PMD.AssignmentToNonFinalStatic") + public CdromController(Channel componentChannel, QemuMonitor monitor) { + super(componentChannel); + if (mapper == null) { + mapper = new ObjectMapper(); + try { + openTray = mapper.readValue("{ \"execute\": " + + "\"blockdev-open-tray\",\"arguments\": {" + + "\"id\": \"\" } }", JsonNode.class); + removeMedium = mapper.readValue("{ \"execute\": " + + "\"blockdev-remove-medium\",\"arguments\": {" + + "\"id\": \"\" } }", JsonNode.class); + changeMedium = mapper.readValue("{ \"execute\": " + + "\"blockdev-change-medium\",\"arguments\": {" + + "\"id\": \"\",\"filename\": \"\"," + + "\"format\": \"raw\",\"read-only-mode\": " + + "\"read-only\" } }", JsonNode.class); + } catch (IOException e) { + logger.log(Level.SEVERE, e, + () -> "Cannot initialize class: " + e.getMessage()); + } + } + this.monitor = monitor; + } + + /** + * On monitor command. + * + * @param event the event + */ + @Handler + @SuppressWarnings("PMD.AvoidDuplicateLiterals") + public void onChangeMediumCommand(ChangeMediumCommand event) { + if (event.command() != Command.CHANGE_MEDIUM) { + return; + } + if (event.file() == null || event.file().isEmpty()) { + var msg = openTray.deepCopy(); + ((ObjectNode) msg.get("arguments")).put("id", event.id()); + monitor.sendToMonitor(msg); + msg = removeMedium.deepCopy(); + ((ObjectNode) msg.get("arguments")).put("id", event.id()); + monitor.sendToMonitor(msg); + fire(new MonitorCommandCompleted(event.command(), null)); + return; + } + var msg = changeMedium.deepCopy(); + ((ObjectNode) msg.get("arguments")).put("id", event.id()); + ((ObjectNode) msg.get("arguments")).put("filename", event.file()); + monitor.sendToMonitor(msg); + fire(new MonitorCommandCompleted(event.command(), null)); + } + +} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java index 2d8e7c4..9442c2a 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java @@ -312,9 +312,11 @@ class Configuration implements Dto { return true; } + @SuppressWarnings("PMD.AvoidLiteralsInIfCondition") private void checkDrives() { for (Drive drive : vm.drives) { - if (drive.file != null || drive.device != null) { + if (drive.file != null || drive.device != null + || "ide-cd".equals(drive.type)) { continue; } if (drive.resource == null) { diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/QemuMonitor.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/QemuMonitor.java index 7125a88..69df213 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/QemuMonitor.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/QemuMonitor.java @@ -19,7 +19,6 @@ package org.jdrupes.vmoperator.runner.qemu; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -106,6 +105,7 @@ public class QemuMonitor extends Component { } attach(new RamController(channel(), this)); attach(new CpuController(channel(), this)); + attach(new CdromController(channel(), this)); } /** @@ -246,7 +246,7 @@ public class QemuMonitor extends Component { fire(new MonitorReady()); return; } - if (response.has("return")) { + if (response.has("return") || response.has("error")) { String executed = executing.poll(); logger.fine( () -> String.format("(Previous \"monitor(in)\" is result " diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java index d2a2d64..e672fd5 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java @@ -41,6 +41,7 @@ import java.nio.file.Paths; import java.util.Comparator; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.logging.Level; import java.util.logging.LogManager; @@ -51,6 +52,7 @@ import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import org.jdrupes.vmoperator.runner.qemu.StateController.State; +import org.jdrupes.vmoperator.runner.qemu.events.ChangeMediumCommand; import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand; import static org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand.Command.CONTINUE; import static org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand.Command.SET_CURRENT_CPUS; @@ -155,7 +157,8 @@ import org.jgrapes.util.events.WatchFile; * @enduml * */ -@SuppressWarnings({ "PMD.ExcessiveImports", "PMD.AvoidPrintStackTrace" }) +@SuppressWarnings({ "PMD.ExcessiveImports", "PMD.AvoidPrintStackTrace", + "PMD.DataflowAnomalyAnalysis" }) public class Runner extends Component { public static final String APP_NAME = "vmrunner"; @@ -331,37 +334,49 @@ public class Runner extends Component { return yamlMapper.readValue(out.toString(), JsonNode.class); } - @SuppressWarnings("unchecked") + @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition", + "PMD.AvoidInstantiatingObjectsInLoops" }) private void updateConfiguration(Map conf) { logger.fine(() -> "Updating configuration"); - Optional.ofNullable((Map) conf.get("vm")) - .map(vm -> vm.get("currentRam")).map(Configuration::parseMemory) - .ifPresent(cr -> { - if (config.vm.currentRam != null - && config.vm.currentRam.equals(cr)) { - return; + var newConf = yamlMapper.convertValue(conf, Configuration.class); + Optional.ofNullable(newConf.vm.currentRam).ifPresent(cr -> { + if (config.vm.currentRam != null + && config.vm.currentRam.equals(cr)) { + return; + } + synchronized (state) { + config.vm.currentRam = cr; + if (state.get() == State.RUNNING) { + fire(new MonitorCommand(SET_CURRENT_RAM, cr)); } + } + }); + if (config.vm.currentCpus != newConf.vm.currentCpus) { + synchronized (state) { + config.vm.currentCpus = newConf.vm.currentCpus; + if (state.get() == State.RUNNING) { + fire(new MonitorCommand(SET_CURRENT_CPUS, + newConf.vm.currentCpus)); + } + } + } + + int cdCounter = 0; + for (int i = 0; i < Math.min(config.vm.drives.length, + newConf.vm.drives.length); i++) { + if (!"ide-cd".equals(config.vm.drives[i].type)) { + continue; + } + String curFile = config.vm.drives[i].file; + String newFile = newConf.vm.drives[i].file; + if (!Objects.equals(curFile, newFile)) { + config.vm.drives[i].file = newConf.vm.drives[i].file; synchronized (state) { - config.vm.currentRam = cr; - if (state.get() == State.RUNNING) { - fire(new MonitorCommand(SET_CURRENT_RAM, cr)); - } + fire(new ChangeMediumCommand("cd" + cdCounter, newFile)); } - }); - Optional.ofNullable((Map) conf.get("vm")) - .map(vm -> vm.get("currentCpus")) - .map(v -> v instanceof Number number ? number.intValue() : null) - .ifPresent(cpus -> { - if (config.vm.currentCpus == cpus) { - return; - } - synchronized (state) { - config.vm.currentCpus = cpus; - if (state.get() == State.RUNNING) { - fire(new MonitorCommand(SET_CURRENT_CPUS, cpus)); - } - } - }); + } + cdCounter += 1; + } } /** diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/ChangeMediumCommand.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/ChangeMediumCommand.java new file mode 100644 index 0000000..f3f1d73 --- /dev/null +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/ChangeMediumCommand.java @@ -0,0 +1,54 @@ +/* + * VM-Operator + * Copyright (C) 2023 Michael N. Lipp + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jdrupes.vmoperator.runner.qemu.events; + +/** + * The Class ChangeMediumCommand. + */ +public class ChangeMediumCommand extends MonitorCommand { + + /** + * Instantiates a new change medium command. + * + * @param id the id + * @param file the file path + */ + public ChangeMediumCommand(String id, String file) { + super(Command.CHANGE_MEDIUM, id, file); + } + + /** + * Gets the id. + * + * @return the id + */ + @SuppressWarnings("PMD.ShortMethodName") + public String id() { + return (String) arguments()[0]; + } + + /** + * Gets the file. + * + * @return the file + */ + public String file() { + return (String) arguments()[1]; + } +} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorCommand.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorCommand.java index 9729d88..a5a9011 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorCommand.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorCommand.java @@ -32,7 +32,7 @@ public class MonitorCommand extends Event { * The available commands. */ public enum Command { - CONTINUE, SET_CURRENT_CPUS, SET_CURRENT_RAM + CONTINUE, SET_CURRENT_CPUS, SET_CURRENT_RAM, CHANGE_MEDIUM } private final Command command; diff --git a/org.jdrupes.vmoperator.runner.qemu/templates/Standard-VM-latest.ftl.yaml b/org.jdrupes.vmoperator.runner.qemu/templates/Standard-VM-latest.ftl.yaml index 8b49f25..3d526ca 100644 --- a/org.jdrupes.vmoperator.runner.qemu/templates/Standard-VM-latest.ftl.yaml +++ b/org.jdrupes.vmoperator.runner.qemu/templates/Standard-VM-latest.ftl.yaml @@ -144,8 +144,8 @@ <#assign cdCounter = 0/> <#list vm.drives![] as drive> <#if (drive.type!"") == "ide-cd"> - - [ "-drive", "id=drive-cdrom${ cdCounter },if=none,media=cdrom,cache=none\ - <#if drive.file??>,file=${ drive.file }" ] + - [ "-drive", "id=drive-cdrom${ cdCounter },if=none,media=cdrom,\ + readonly=on<#if drive.file??>,file=${ drive.file }" ] # (IDE is old, but faster than usb-storage. virtio-blk-pci does not # work without file [empty drive]) - [ "-device", "ide-cd,id=cd${ cdCounter },bus=ide.${ cdCounter },\