Merge branch 'main' into testing

This commit is contained in:
Michael Lipp 2025-03-12 17:48:35 +01:00
commit 4aa2d6be5c
12 changed files with 150 additions and 112 deletions

View file

@ -5,8 +5,8 @@
# Run Qemu in Kubernetes Pods # Run Qemu in Kubernetes Pods
The goal of this project is to provide orgy to use and flexible components The goal of this project is to provide simply to use and flexible components
for running Qemu based VMs in Kubernetes pods. for running Qemu based VMs in Kubernetes pods.
vm-ovm
See the [project's home page](https://vm-operator.jdrupes.org/) See the [project's home page](https://vm-operator.jdrupes.org/)
for details. for details.

View file

@ -8,12 +8,9 @@ metadata:
spec: spec:
image: image:
# pullPolicy: Always # source: ghcr.io/mnlipp/org.jdrupes.vmoperator.runner.qemu-arch:3.3.1
# repository: ghcr.io
# path: mnlipp/org.jdrupes.vmoperator.runner.qemu-alpine
# version: "3.0.0"
# source: docker-registry.lan.mnl.de/vm-operator/org.jdrupes.vmoperator.runner.qemu-arch:feature-auto-login
source: registry.mnl.de/org/jdrupes/vm-operator/org.jdrupes.vmoperator.runner.qemu-arch:testing source: registry.mnl.de/org/jdrupes/vm-operator/org.jdrupes.vmoperator.runner.qemu-arch:testing
# source: docker-registry.lan.mnl.de/vmoperator/org.jdrupes.vmoperator.runner.qemu-arch:feature-pools
pullPolicy: Always pullPolicy: Always
permissions: permissions:
@ -34,8 +31,8 @@ spec:
bootMenu: true bootMenu: true
maximumCpus: 4 maximumCpus: 4
currentCpus: 2 currentCpus: 2
maximumRam: 4Gi maximumRam: 6Gi
currentRam: 3Gi currentRam: 4Gi
networks: networks:
# No bridge on TC1 # No bridge on TC1

View file

@ -1,5 +1,8 @@
#!/usr/bin/bash #!/usr/bin/bash
# Note that this script requires "jq" to be installed and a version
# of loginctl that accepts the "-j" option.
while [ "$#" -gt 0 ]; do while [ "$#" -gt 0 ]; do
case "$1" in case "$1" in
--path) shift; ttyPath="$1";; --path) shift; ttyPath="$1";;

View file

@ -20,10 +20,10 @@ package org.jdrupes.vmoperator.runner.qemu;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List;
import org.jdrupes.vmoperator.runner.qemu.events.VserportChangeEvent; import org.jdrupes.vmoperator.runner.qemu.events.VserportChangeEvent;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
import org.jgrapes.core.annotation.Handler; import org.jgrapes.core.annotation.Handler;
import org.jgrapes.util.events.ConfigurationUpdate;
/** /**
* A component that handles the communication with an agent * A component that handles the communication with an agent
@ -47,19 +47,41 @@ public abstract class AgentConnector extends QemuConnector {
} }
/** /**
* As the initial configuration of this component depends on the * Extracts the channel id and the socket path from the QEMU
* configuration of the {@link Runner}, it doesn't have a handler * command line.
* for the {@link ConfigurationUpdate} event. The values are
* forwarded from the {@link Runner} instead.
* *
* @param channelId the channel id * @param command the command
* @param socketPath the socket path * @param chardev the chardev
*/ */
/* default */ void configure(String channelId, Path socketPath) { @SuppressWarnings("PMD.CognitiveComplexity")
super.configure(socketPath); protected void configureConnection(List<String> command, String chardev) {
this.channelId = channelId; Path socketPath = null;
for (var arg : command) {
if (arg.startsWith("virtserialport,")
&& arg.contains("chardev=" + chardev)) {
for (var prop : arg.split(",")) {
if (prop.startsWith("id=")) {
channelId = prop.substring(3);
}
}
}
if (arg.startsWith("socket,")
&& arg.contains("id=" + chardev)) {
for (var prop : arg.split(",")) {
if (prop.startsWith("path=")) {
socketPath = Path.of(prop.substring(5));
}
}
}
}
if (channelId == null || socketPath == null) {
logger.warning(() -> "Definition of chardev " + chardev
+ " missing in runner template.");
return;
}
logger.fine(() -> getClass().getSimpleName() + " configured with" logger.fine(() -> getClass().getSimpleName() + " configured with"
+ " channelId=" + channelId); + " channelId=" + channelId);
super.configure(socketPath);
} }
/** /**

View file

@ -49,6 +49,7 @@ public class DisplayController extends Component {
private String currentPassword; private String currentPassword;
private String protocol; private String protocol;
private final Path configDir; private final Path configDir;
private boolean canBeUpdated;
private boolean vmopAgentConnected; private boolean vmopAgentConnected;
private String loggedInUser; private String loggedInUser;
@ -83,6 +84,7 @@ public class DisplayController extends Component {
if (event.runState() == RunState.STARTING) { if (event.runState() == RunState.STARTING) {
configurePassword(); configurePassword();
} }
canBeUpdated = true;
} }
/** /**
@ -114,7 +116,8 @@ public class DisplayController extends Component {
@Handler @Handler
@SuppressWarnings("PMD.EmptyCatchBlock") @SuppressWarnings("PMD.EmptyCatchBlock")
public void onFileChanged(FileChanged event) { public void onFileChanged(FileChanged event) {
if (event.path().equals(configDir.resolve(DisplaySecret.PASSWORD))) { if (event.path().equals(configDir.resolve(DisplaySecret.PASSWORD))
&& canBeUpdated) {
configurePassword(); configurePassword();
} }
} }

View file

@ -56,7 +56,7 @@ public class GuestAgentClient extends AgentConnector {
*/ */
@Override @Override
protected void agentConnected() { protected void agentConnected() {
fire(new GuestAgentCommand(new QmpGuestGetOsinfo())); rep().fire(new GuestAgentCommand(new QmpGuestGetOsinfo()));
} }
/** /**

View file

@ -121,7 +121,7 @@ public abstract class QemuConnector extends Component {
// qemu running, open socket // qemu running, open socket
fire(new OpenSocketConnection( fire(new OpenSocketConnection(
UnixDomainSocketAddress.of(socketPath)) UnixDomainSocketAddress.of(socketPath))
.setAssociated(getClass(), this)); .setAssociated(this, this));
} }
} }
@ -137,21 +137,21 @@ public abstract class QemuConnector extends Component {
@Handler @Handler
public void onClientConnected(ClientConnected event, public void onClientConnected(ClientConnected event,
SocketIOChannel channel) { SocketIOChannel channel) {
event.openEvent().associated(getClass()).ifPresent(qm -> { event.openEvent().associated(this, getClass()).ifPresent(qc -> {
qemuChannel = channel; qemuChannel = channel;
channel.setAssociated(getClass(), this); channel.setAssociated(this, this);
channel.setAssociated(Writer.class, new ByteBufferWriter( channel.setAssociated(Writer.class, new ByteBufferWriter(
channel).nativeCharset()); channel).nativeCharset());
channel.setAssociated(LineCollector.class, channel.setAssociated(LineCollector.class,
new LineCollector() new LineCollector()
.consumer(line -> { .consumer(line -> {
try { try {
processInput(line); qc.processInput(line);
} catch (IOException e) { } catch (IOException e) {
throw new UndeclaredThrowableException(e); throw new UndeclaredThrowableException(e);
} }
})); }));
socketConnected(); qc.socketConnected();
}); });
} }
@ -202,11 +202,10 @@ public abstract class QemuConnector extends Component {
* Called when a connection attempt fails. * Called when a connection attempt fails.
* *
* @param event the event * @param event the event
* @param channel the channel
*/ */
@Handler @Handler
public void onConnectError(ConnectError event, SocketIOChannel channel) { public void onConnectError(ConnectError event) {
event.event().associated(getClass()).ifPresent(qm -> { event.event().associated(this, getClass()).ifPresent(qc -> {
rep.fire(new Stop()); rep.fire(new Stop());
}); });
} }
@ -219,7 +218,7 @@ public abstract class QemuConnector extends Component {
*/ */
@Handler @Handler
public void onInput(Input<?> event, SocketIOChannel channel) { public void onInput(Input<?> event, SocketIOChannel channel) {
if (channel.associated(getClass()).isEmpty()) { if (channel.associated(this, getClass()).isEmpty()) {
return; return;
} }
channel.associated(LineCollector.class).ifPresent(collector -> { channel.associated(LineCollector.class).ifPresent(collector -> {
@ -243,7 +242,7 @@ public abstract class QemuConnector extends Component {
*/ */
@Handler @Handler
public void onClosed(Closed<?> event, SocketIOChannel channel) { public void onClosed(Closed<?> event, SocketIOChannel channel) {
channel.associated(getClass()).ifPresent(qm -> { channel.associated(this, getClass()).ifPresent(qm -> {
qemuChannel = null; qemuChannel = null;
}); });
} }

View file

@ -61,6 +61,7 @@ public class QemuMonitor extends QemuConnector {
private Stop suspendedStop; private Stop suspendedStop;
private Timer powerdownTimer; private Timer powerdownTimer;
private boolean powerdownConfirmed; private boolean powerdownConfirmed;
private boolean monitorReady;
/** /**
* Instantiates a new QEMU monitor. * Instantiates a new QEMU monitor.
@ -99,7 +100,7 @@ public class QemuMonitor extends QemuConnector {
*/ */
@Override @Override
protected void socketConnected() { protected void socketConnected() {
fire(new MonitorCommand(new QmpCapabilities())); rep().fire(new MonitorCommand(new QmpCapabilities()));
} }
@Override @Override
@ -109,6 +110,7 @@ public class QemuMonitor extends QemuConnector {
try { try {
var response = mapper.readValue(line, ObjectNode.class); var response = mapper.readValue(line, ObjectNode.class);
if (response.has("QMP")) { if (response.has("QMP")) {
monitorReady = true;
rep().fire(new MonitorReady()); rep().fire(new MonitorReady());
return; return;
} }
@ -137,8 +139,9 @@ public class QemuMonitor extends QemuConnector {
@SuppressWarnings({ "PMD.AvoidSynchronizedStatement", @SuppressWarnings({ "PMD.AvoidSynchronizedStatement",
"PMD.AvoidDuplicateLiterals" }) "PMD.AvoidDuplicateLiterals" })
public void onClosed(Closed<?> event, SocketIOChannel channel) { public void onClosed(Closed<?> event, SocketIOChannel channel) {
logger.finer(() -> "Closing QMP socket.");
super.onClosed(event, channel); super.onClosed(event, channel);
channel.associated(QemuMonitor.class).ifPresent(qm -> { channel.associated(this, getClass()).ifPresent(qm -> {
synchronized (this) { synchronized (this) {
if (powerdownTimer != null) { if (powerdownTimer != null) {
powerdownTimer.cancel(); powerdownTimer.cancel();
@ -149,6 +152,7 @@ public class QemuMonitor extends QemuConnector {
} }
} }
}); });
logger.finer(() -> "QMP socket closed.");
} }
/** /**
@ -158,9 +162,17 @@ public class QemuMonitor extends QemuConnector {
* @throws IOException * @throws IOException
*/ */
@Handler @Handler
@SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition", @SuppressWarnings("PMD.AvoidSynchronizedStatement")
"PMD.AvoidSynchronizedStatement" }) public void onMonitorCommand(MonitorCommand event) throws IOException {
public void onExecQmpCommand(MonitorCommand event) throws IOException { // Check prerequisites
if (!monitorReady && !(event.command() instanceof QmpCapabilities)) {
logger.severe(() -> "Premature monitor command (not ready): "
+ event.command());
rep().fire(new Stop());
return;
}
// Send the command
var command = event.command(); var command = event.command();
logger.fine(() -> "monitor(out): " + command.toString()); logger.fine(() -> "monitor(out): " + command.toString());
String asText; String asText;

View file

@ -214,12 +214,14 @@ public class Runner extends Component {
@SuppressWarnings("PMD.UseConcurrentHashMap") @SuppressWarnings("PMD.UseConcurrentHashMap")
private final File configFile; private final File configFile;
private final Path configDir; private final Path configDir;
private Configuration config = new Configuration(); private Configuration initialConfig;
private Configuration pendingConfig;
private final freemarker.template.Configuration fmConfig; private final freemarker.template.Configuration fmConfig;
private CommandDefinition swtpmDefinition; private CommandDefinition swtpmDefinition;
private CommandDefinition cloudInitImgDefinition; private CommandDefinition cloudInitImgDefinition;
private CommandDefinition qemuDefinition; private CommandDefinition qemuDefinition;
private final QemuMonitor qemuMonitor; private final QemuMonitor qemuMonitor;
private boolean qmpConfigured;
private final GuestAgentClient guestAgentClient; private final GuestAgentClient guestAgentClient;
private final VmopAgentClient vmopAgentClient; private final VmopAgentClient vmopAgentClient;
private Integer resetCounter; private Integer resetCounter;
@ -301,7 +303,10 @@ public class Runner extends Component {
} }
/** /**
* On configuration update. * Process the initial configuration. The initial configuration
* and any subsequent updates will be forwarded to other components
* only when the QMP connection is ready
* (see @link #onQmpConfigured(QmpConfigured)).
* *
* @param event the event * @param event the event
*/ */
@ -318,27 +323,33 @@ public class Runner extends Component {
// Special actions for initial configuration (startup) // Special actions for initial configuration (startup)
if (event instanceof InitialConfiguration) { if (event instanceof InitialConfiguration) {
processInitialConfiguration(newConf); processInitialConfiguration(newConf);
return;
} }
logger.fine(() -> "Updating configuration");
rep.fire(new ConfigureQemu(newConf, state)); // Check if to be sent immediately or later
if (qmpConfigured) {
rep.fire(new ConfigureQemu(newConf, state));
} else {
pendingConfig = newConf;
}
}); });
} }
@SuppressWarnings("PMD.LambdaCanBeMethodReference") @SuppressWarnings("PMD.LambdaCanBeMethodReference")
private void processInitialConfiguration(Configuration newConfig) { private void processInitialConfiguration(Configuration newConfig) {
try { try {
config = newConfig; if (!newConfig.check()) {
if (!config.check()) {
// Invalid configuration, not used, problems already logged. // Invalid configuration, not used, problems already logged.
config = null; return;
} }
// Prepare firmware files and add to config // Prepare firmware files and add to config
setFirmwarePaths(); setFirmwarePaths(newConfig);
// Obtain more context data from template // Obtain more context data from template
var tplData = dataFromTemplate(); var tplData = dataFromTemplate(newConfig);
initialConfig = newConfig;
// Configure
swtpmDefinition = Optional.ofNullable(tplData.get(SWTPM)) swtpmDefinition = Optional.ofNullable(tplData.get(SWTPM))
.map(d -> new CommandDefinition(SWTPM, d)).orElse(null); .map(d -> new CommandDefinition(SWTPM, d)).orElse(null);
logger.finest(() -> swtpmDefinition.toString()); logger.finest(() -> swtpmDefinition.toString());
@ -352,21 +363,21 @@ public class Runner extends Component {
logger.finest(() -> cloudInitImgDefinition.toString()); logger.finest(() -> cloudInitImgDefinition.toString());
// Forward some values to child components // Forward some values to child components
qemuMonitor.configure(config.monitorSocket, qemuMonitor.configure(initialConfig.monitorSocket,
config.vm.powerdownTimeout); initialConfig.vm.powerdownTimeout);
configureAgentClient(guestAgentClient, "guest-agent-socket"); guestAgentClient.configureConnection(qemuDefinition.command,
configureAgentClient(vmopAgentClient, "vmop-agent-socket"); "guest-agent-socket");
vmopAgentClient.configureConnection(qemuDefinition.command,
"vmop-agent-socket");
} catch (IllegalArgumentException | IOException | TemplateException e) { } catch (IllegalArgumentException | IOException | TemplateException e) {
logger.log(Level.SEVERE, e, () -> "Invalid configuration: " logger.log(Level.SEVERE, e, () -> "Invalid configuration: "
+ e.getMessage()); + e.getMessage());
// Don't use default configuration
config = null;
} }
} }
@SuppressWarnings({ "PMD.CognitiveComplexity", @SuppressWarnings({ "PMD.CognitiveComplexity",
"PMD.DataflowAnomalyAnalysis" }) "PMD.DataflowAnomalyAnalysis" })
private void setFirmwarePaths() throws IOException { private void setFirmwarePaths(Configuration config) throws IOException {
JsonNode firmware = defaults.path("firmware").path(config.vm.firmware); JsonNode firmware = defaults.path("firmware").path(config.vm.firmware);
// Get file for firmware ROM // Get file for firmware ROM
JsonNode codePaths = firmware.path("rom"); JsonNode codePaths = firmware.path("rom");
@ -396,7 +407,7 @@ public class Runner extends Component {
} }
} }
private JsonNode dataFromTemplate() private JsonNode dataFromTemplate(Configuration config)
throws IOException, TemplateNotFoundException, throws IOException, TemplateNotFoundException,
MalformedTemplateNameException, ParseException, TemplateException, MalformedTemplateNameException, ParseException, TemplateException,
JsonProcessingException, JsonMappingException { JsonProcessingException, JsonMappingException {
@ -435,6 +446,21 @@ public class Runner extends Component {
return yamlMapper.readValue(out.toString(), JsonNode.class); return yamlMapper.readValue(out.toString(), JsonNode.class);
} }
/**
* Note ready state and send a {@link ConfigureQemu} event for
* any pending configuration (initial or change).
*
* @param event the event
*/
@Handler
public void onQmpConfigured(QmpConfigured event) {
qmpConfigured = true;
if (pendingConfig != null) {
rep.fire(new ConfigureQemu(pendingConfig, state));
pendingConfig = null;
}
}
/** /**
* Handle the start event. * Handle the start event.
* *
@ -442,7 +468,7 @@ public class Runner extends Component {
*/ */
@Handler(priority = 100) @Handler(priority = 100)
public void onStart(Start event) { public void onStart(Start event) {
if (config == null) { if (initialConfig == null) {
// Missing configuration, fail // Missing configuration, fail
event.cancel(true); event.cancel(true);
fire(new Stop()); fire(new Stop());
@ -458,19 +484,19 @@ public class Runner extends Component {
try { try {
// Store process id // Store process id
try (var pidFile = Files.newBufferedWriter( try (var pidFile = Files.newBufferedWriter(
config.runtimeDir.resolve("runner.pid"))) { initialConfig.runtimeDir.resolve("runner.pid"))) {
pidFile.write(ProcessHandle.current().pid() + "\n"); pidFile.write(ProcessHandle.current().pid() + "\n");
} }
// Files to watch for // Files to watch for
Files.deleteIfExists(config.swtpmSocket); Files.deleteIfExists(initialConfig.swtpmSocket);
fire(new WatchFile(config.swtpmSocket)); fire(new WatchFile(initialConfig.swtpmSocket));
// Helper files // Helper files
var ticket = Optional.ofNullable(config.vm.display) var ticket = Optional.ofNullable(initialConfig.vm.display)
.map(d -> d.spice).map(s -> s.ticket); .map(d -> d.spice).map(s -> s.ticket);
if (ticket.isPresent()) { if (ticket.isPresent()) {
Files.write(config.runtimeDir.resolve("ticket.txt"), Files.write(initialConfig.runtimeDir.resolve("ticket.txt"),
ticket.get().getBytes()); ticket.get().getBytes());
} }
} catch (IOException e) { } catch (IOException e) {
@ -480,36 +506,6 @@ public class Runner extends Component {
} }
} }
@SuppressWarnings("PMD.CognitiveComplexity")
private void configureAgentClient(AgentConnector client, String chardev) {
String id = null;
Path path = null;
for (var arg : qemuDefinition.command) {
if (arg.startsWith("virtserialport,")
&& arg.contains("chardev=" + chardev)) {
for (var prop : arg.split(",")) {
if (prop.startsWith("id=")) {
id = prop.substring(3);
}
}
}
if (arg.startsWith("socket,")
&& arg.contains("id=" + chardev)) {
for (var prop : arg.split(",")) {
if (prop.startsWith("path=")) {
path = Path.of(prop.substring(5));
}
}
}
}
if (id == null || path == null) {
logger.warning(() -> "Definition of chardev " + chardev
+ " missing in runner template.");
return;
}
client.configure(id, path);
}
/** /**
* Handle the started event. * Handle the started event.
* *
@ -522,12 +518,12 @@ public class Runner extends Component {
"Runner has been started")); "Runner has been started"));
// Start first process(es) // Start first process(es)
qemuLatch.add(QemuPreps.Config); qemuLatch.add(QemuPreps.Config);
if (config.vm.useTpm && swtpmDefinition != null) { if (initialConfig.vm.useTpm && swtpmDefinition != null) {
startProcess(swtpmDefinition); startProcess(swtpmDefinition);
qemuLatch.add(QemuPreps.Tpm); qemuLatch.add(QemuPreps.Tpm);
} }
if (config.cloudInit != null) { if (initialConfig.cloudInit != null) {
generateCloudInitImg(); generateCloudInitImg(initialConfig);
qemuLatch.add(QemuPreps.CloudInit); qemuLatch.add(QemuPreps.CloudInit);
} }
mayBeStartQemu(QemuPreps.Config); mayBeStartQemu(QemuPreps.Config);
@ -546,7 +542,7 @@ public class Runner extends Component {
} }
} }
private void generateCloudInitImg() { private void generateCloudInitImg(Configuration config) {
try { try {
var cloudInitDir = config.dataDir.resolve("cloud-init"); var cloudInitDir = config.dataDir.resolve("cloud-init");
cloudInitDir.toFile().mkdir(); cloudInitDir.toFile().mkdir();
@ -583,7 +579,7 @@ public class Runner extends Component {
private boolean startProcess(CommandDefinition toStart) { private boolean startProcess(CommandDefinition toStart) {
logger.info( logger.info(
() -> "Starting process: " + String.join(" ", toStart.command)); () -> "Starting process: " + String.join(" ", toStart.command));
fire(new StartProcess(toStart.command) rep.fire(new StartProcess(toStart.command)
.setAssociated(CommandDefinition.class, toStart)); .setAssociated(CommandDefinition.class, toStart));
return true; return true;
} }
@ -597,7 +593,7 @@ public class Runner extends Component {
@Handler @Handler
public void onFileChanged(FileChanged event) { public void onFileChanged(FileChanged event) {
if (event.change() == Kind.CREATED if (event.change() == Kind.CREATED
&& event.path().equals(config.swtpmSocket)) { && event.path().equals(initialConfig.swtpmSocket)) {
// swtpm running, maybe start qemu // swtpm running, maybe start qemu
mayBeStartQemu(QemuPreps.Tpm); mayBeStartQemu(QemuPreps.Tpm);
} }
@ -620,7 +616,7 @@ public class Runner extends Component {
.ifPresent(procDef -> { .ifPresent(procDef -> {
channel.setAssociated(CommandDefinition.class, procDef); channel.setAssociated(CommandDefinition.class, procDef);
try (var pidFile = Files.newBufferedWriter( try (var pidFile = Files.newBufferedWriter(
config.runtimeDir.resolve(procDef.name + ".pid"))) { initialConfig.runtimeDir.resolve(procDef.name + ".pid"))) {
pidFile.write(channel.process().toHandle().pid() + "\n"); pidFile.write(channel.process().toHandle().pid() + "\n");
} catch (IOException e) { } catch (IOException e) {
throw new UndeclaredThrowableException(e); throw new UndeclaredThrowableException(e);
@ -652,16 +648,6 @@ public class Runner extends Component {
.ifPresent(lc -> lc.feed(event))); .ifPresent(lc -> lc.feed(event)));
} }
/**
* When the monitor is ready, send QEMU its initial configuration.
*
* @param event the event
*/
@Handler
public void onQmpConfigured(QmpConfigured event) {
rep.fire(new ConfigureQemu(config, state));
}
/** /**
* Whenever a new QEMU configuration is available, check if it * Whenever a new QEMU configuration is available, check if it
* is supposed to trigger a reset. * is supposed to trigger a reset.
@ -791,7 +777,7 @@ public class Runner extends Component {
logger.log(Level.WARNING, e, () -> "Proper shutdown failed."); logger.log(Level.WARNING, e, () -> "Proper shutdown failed.");
} }
Optional.ofNullable(config).map(c -> c.runtimeDir) Optional.ofNullable(initialConfig).map(c -> c.runtimeDir)
.ifPresent(runtimeDir -> { .ifPresent(runtimeDir -> {
try { try {
Files.walk(runtimeDir).sorted(Comparator.reverseOrder()) Files.walk(runtimeDir).sorted(Comparator.reverseOrder())

View file

@ -73,6 +73,7 @@
<li><p class="part-entry"><a href="auto-login.html">Auto Login</a></p></li> <li><p class="part-entry"><a href="auto-login.html">Auto Login</a></p></li>
<li><p class="part-entry"><a href="pools.html">Pools</a></p></li> <li><p class="part-entry"><a href="pools.html">Pools</a></p></li>
</ul> </ul>
<p class="part-list-title"><a href="hints.html">Hints</a></p>
<p class="part-list-title"><a href="upgrading.html">Upgrading</a></p> <p class="part-list-title"><a href="upgrading.html">Upgrading</a></p>
<p class="part-list-title"><a href="https://vm-operator.jdrupes.org/javadoc/index.html">Javadoc</a></p> <p class="part-list-title"><a href="https://vm-operator.jdrupes.org/javadoc/index.html">Javadoc</a></p>

16
webpages/hints.md Normal file
View file

@ -0,0 +1,16 @@
---
title: "VM-Operator: Hints — Miscellaneous hints for using VM-Operator"
layout: vm-operator
---
# Hints
## Disable suspend and hibernate
Suspend and hibernate are poorly supported in VMs and usually do not
work as expected. To disable these on systemd based systems, use the
following command:
```console
# systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target
```

View file

@ -1,5 +1,5 @@
--- ---
title: "Run VMs on Kubernetes using QEMU/KVM and SPICE" title: "VM-Operator: Easy to use kubernetes operator for QEM/KVM VMs"
description: >- description: >-
A solution for running VMs on Kubernetes with a web interface for A solution for running VMs on Kubernetes with a web interface for
admins and users. Focuses on running QEMU/KVM virtual machines and admins and users. Focuses on running QEMU/KVM virtual machines and
@ -60,8 +60,7 @@ A second look, however, reveals that Kubernetes has more to offer.
* It has a well defined API for managing resources. * It has a well defined API for managing resources.
* It provides access to different kinds of managed storage for the VMs. * It provides access to different kinds of managed storage for the VMs.
* Its managing features *are* useful for running the component that * Its managing features *are* useful for running the component that
manages the pods with the VMs.
manages the pods with the VMs.
And if you use Kubernetes anyway, well then the VMs within Kubernetes And if you use Kubernetes anyway, well then the VMs within Kubernetes
provide you with a unified view of all (or most of) your workloads, provide you with a unified view of all (or most of) your workloads,