Support cdrom media change.

This commit is contained in:
Michael Lipp 2023-08-05 11:40:36 +02:00
parent ae2e6cde7f
commit e8b10b32b0
12 changed files with 278 additions and 37 deletions

View file

@ -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/<group>/<version>/<plural>
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

View file

@ -431,10 +431,24 @@ spec:
type: string
type: object
type: object
cdromImage:
type: object
properties:
path:
type: string
required:
- path
bootindex:
type: integer
oneOf:
- properties:
volumeClaimTemplate:
required:
- volumeClaimTemplate
- properties:
cdromImage:
required:
- cdromImage
default: []
display:
type: object

View file

@ -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>
<#if disk.volumeClaimTemplate??>
- type: raw
resource: /dev/disk-${ name }
<#if disk.bootindex??>
bootindex: ${ disk.bootindex.asInt?c }
</#if>
<#assign drvCounter = drvCounter + 1/>
</#if>
<#if disk.cdromImage??>
- type: ide-cd
file: "${ disk.cdromImage.path.asString }"
<#if disk.bootindex??>
bootindex: ${ disk.bootindex.asInt?c }
</#if>
</#if>
</#list>
display:

View file

@ -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/>
</#if>
</#list>
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/>
</#if>
</#list>
hostNetwork: true
terminationGracePeriodSeconds: ${ (cr.spec.vm.powerdownTimeout.asInt + 5)?c }

View file

@ -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());

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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));
}
}

View file

@ -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) {

View file

@ -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 "

View file

@ -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,12 +334,12 @@ public class Runner extends Component {
return yamlMapper.readValue(out.toString(), JsonNode.class);
}
@SuppressWarnings("unchecked")
@SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition",
"PMD.AvoidInstantiatingObjectsInLoops" })
private void updateConfiguration(Map<String, Object> conf) {
logger.fine(() -> "Updating configuration");
Optional.ofNullable((Map<String, Object>) conf.get("vm"))
.map(vm -> vm.get("currentRam")).map(Configuration::parseMemory)
.ifPresent(cr -> {
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;
@ -348,20 +351,32 @@ public class Runner extends Component {
}
}
});
Optional.ofNullable((Map<String, Object>) 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;
}
if (config.vm.currentCpus != newConf.vm.currentCpus) {
synchronized (state) {
config.vm.currentCpus = cpus;
config.vm.currentCpus = newConf.vm.currentCpus;
if (state.get() == State.RUNNING) {
fire(new MonitorCommand(SET_CURRENT_CPUS, cpus));
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) {
fire(new ChangeMediumCommand("cd" + cdCounter, newFile));
}
}
cdCounter += 1;
}
}
/**

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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];
}
}

View file

@ -32,7 +32,7 @@ public class MonitorCommand extends Event<Void> {
* 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;

View file

@ -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 }</#if>" ]
- [ "-drive", "id=drive-cdrom${ cdCounter },if=none,media=cdrom,\
readonly=on<#if drive.file??>,file=${ drive.file }</#if>" ]
# (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 },\