diff --git a/dev-example/config.yaml b/dev-example/config.yaml index 2a72bc8..fc1cee7 100644 --- a/dev-example/config.yaml +++ b/dev-example/config.yaml @@ -21,8 +21,8 @@ # Defaults for namespace (VM domain) handlers=java.util.logging.ConsoleHandler - #org.jgrapes.level=FINE - #org.jgrapes.core.handlerTracking.level=FINER + org.jgrapes.level=FINE + org.jgrapes.core.handlerTracking.level=FINER org.jdrupes.vmoperator.runner.qemu.level=FINEST diff --git a/dev-example/test-vm.tpl.yaml b/dev-example/test-vm.tpl.yaml index a6d8825..5e104f8 100644 --- a/dev-example/test-vm.tpl.yaml +++ b/dev-example/test-vm.tpl.yaml @@ -8,12 +8,10 @@ metadata: spec: image: -# pullPolicy: Always -# 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:feature-auto-login + source: ghcr.io/mnlipp/org.jdrupes.vmoperator.runner.qemu-arch:3.3.1 +# source: registry.mnl.de/org/jdrupes/vm-operator/org.jdrupes.vmoperator.runner.qemu-arch:feature-login-pool +# source: docker-registry.lan.mnl.de/vmoperator/org.jdrupes.vmoperator.runner.qemu-arch:feature-pools +# source: docker-registry.lan.mnl.de/vmoperator/org.jdrupes.vmoperator.runner.qemu-arch:fix-race-condition pullPolicy: Always permissions: diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/DisplayController.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/DisplayController.java index d301aac..f1b2c53 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/DisplayController.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/DisplayController.java @@ -29,6 +29,7 @@ import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetDisplayPassword; import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetPasswordExpiry; import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu; import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand; +import org.jdrupes.vmoperator.runner.qemu.events.QmpConfigured; import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState; import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentConnected; import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLogIn; @@ -85,6 +86,19 @@ public class DisplayController extends Component { } } + /** + * When the monitor is ready, send QEMU its initial configuration. + * + * @param event the event + */ + @Handler + public void onQmpConfigured(QmpConfigured event) { + if (pendingConfig != null) { + rep.fire(new ConfigureQemu(pendingConfig, state)); + pendingConfig = null; + } + } + /** * On vmop agent connected. * diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/GuestAgentClient.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/GuestAgentClient.java index 880ca58..ead7dd7 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/GuestAgentClient.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/GuestAgentClient.java @@ -56,7 +56,7 @@ public class GuestAgentClient extends AgentConnector { */ @Override protected void agentConnected() { - fire(new GuestAgentCommand(new QmpGuestGetOsinfo())); + rep().fire(new GuestAgentCommand(new QmpGuestGetOsinfo())); } /** diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/QemuConnector.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/QemuConnector.java index 2e94c14..d3ff86e 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/QemuConnector.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/QemuConnector.java @@ -121,7 +121,7 @@ public abstract class QemuConnector extends Component { // qemu running, open socket fire(new OpenSocketConnection( UnixDomainSocketAddress.of(socketPath)) - .setAssociated(getClass(), this)); + .setAssociated(this, this)); } } @@ -137,21 +137,21 @@ public abstract class QemuConnector extends Component { @Handler public void onClientConnected(ClientConnected event, SocketIOChannel channel) { - event.openEvent().associated(getClass()).ifPresent(qm -> { + event.openEvent().associated(this, getClass()).ifPresent(qc -> { qemuChannel = channel; - channel.setAssociated(getClass(), this); + channel.setAssociated(this, this); channel.setAssociated(Writer.class, new ByteBufferWriter( channel).nativeCharset()); channel.setAssociated(LineCollector.class, new LineCollector() .consumer(line -> { try { - processInput(line); + qc.processInput(line); } catch (IOException e) { throw new UndeclaredThrowableException(e); } })); - socketConnected(); + qc.socketConnected(); }); } @@ -206,7 +206,7 @@ public abstract class QemuConnector extends Component { */ @Handler public void onConnectError(ConnectError event, SocketIOChannel channel) { - event.event().associated(getClass()).ifPresent(qm -> { + event.event().associated(this, getClass()).ifPresent(qc -> { rep.fire(new Stop()); }); } @@ -219,7 +219,7 @@ public abstract class QemuConnector extends Component { */ @Handler public void onInput(Input event, SocketIOChannel channel) { - if (channel.associated(getClass()).isEmpty()) { + if (channel.associated(this, getClass()).isEmpty()) { return; } channel.associated(LineCollector.class).ifPresent(collector -> { @@ -243,7 +243,7 @@ public abstract class QemuConnector extends Component { */ @Handler public void onClosed(Closed event, SocketIOChannel channel) { - channel.associated(getClass()).ifPresent(qm -> { + channel.associated(this, getClass()).ifPresent(qm -> { qemuChannel = 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 1de8f60..4be0aff 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 @@ -61,6 +61,7 @@ public class QemuMonitor extends QemuConnector { private Stop suspendedStop; private Timer powerdownTimer; private boolean powerdownConfirmed; + private boolean monitorReady; /** * Instantiates a new QEMU monitor. @@ -99,7 +100,7 @@ public class QemuMonitor extends QemuConnector { */ @Override protected void socketConnected() { - fire(new MonitorCommand(new QmpCapabilities())); + rep().fire(new MonitorCommand(new QmpCapabilities())); } @Override @@ -109,6 +110,7 @@ public class QemuMonitor extends QemuConnector { try { var response = mapper.readValue(line, ObjectNode.class); if (response.has("QMP")) { + monitorReady = true; rep().fire(new MonitorReady()); return; } @@ -137,8 +139,9 @@ public class QemuMonitor extends QemuConnector { @SuppressWarnings({ "PMD.AvoidSynchronizedStatement", "PMD.AvoidDuplicateLiterals" }) public void onClosed(Closed event, SocketIOChannel channel) { + logger.finer(() -> "Closing QMP socket."); super.onClosed(event, channel); - channel.associated(QemuMonitor.class).ifPresent(qm -> { + channel.associated(this, getClass()).ifPresent(qm -> { synchronized (this) { if (powerdownTimer != null) { powerdownTimer.cancel(); @@ -149,6 +152,7 @@ public class QemuMonitor extends QemuConnector { } } }); + logger.finer(() -> "QMP socket closed."); } /** @@ -158,9 +162,14 @@ public class QemuMonitor extends QemuConnector { * @throws IOException */ @Handler - @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition", - "PMD.AvoidSynchronizedStatement" }) - public void onExecQmpCommand(MonitorCommand event) throws IOException { + @SuppressWarnings("PMD.AvoidSynchronizedStatement") + public void onMonitorCommand(MonitorCommand event) throws IOException { + if (!monitorReady) { + logger.severe(() -> "Premature monitor command (not ready): " + + event.command()); + rep().fire(new Stop()); + return; + } var command = event.command(); logger.fine(() -> "monitor(out): " + command.toString()); String asText; 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 01c4127..e3aa70a 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 @@ -192,7 +192,7 @@ import org.jgrapes.util.events.WatchFile; */ @SuppressWarnings({ "PMD.ExcessiveImports", "PMD.AvoidPrintStackTrace", "PMD.DataflowAnomalyAnalysis", "PMD.TooManyMethods", - "PMD.CouplingBetweenObjects", "PMD.TooManyFields" }) + "PMD.CouplingBetweenObjects", "PMD.TooManyFields", "PMD.GodClass" }) public class Runner extends Component { private static final String QEMU = "qemu"; @@ -214,12 +214,14 @@ public class Runner extends Component { @SuppressWarnings("PMD.UseConcurrentHashMap") private final File configFile; private final Path configDir; - private Configuration config = new Configuration(); + private Configuration initialConfig; + private Configuration pendingConfig; private final freemarker.template.Configuration fmConfig; private CommandDefinition swtpmDefinition; private CommandDefinition cloudInitImgDefinition; private CommandDefinition qemuDefinition; private final QemuMonitor qemuMonitor; + private boolean qmpConfigured; private final GuestAgentClient guestAgentClient; private final VmopAgentClient vmopAgentClient; private Integer resetCounter; @@ -318,27 +320,33 @@ public class Runner extends Component { // Special actions for initial configuration (startup) if (event instanceof InitialConfiguration) { 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") private void processInitialConfiguration(Configuration newConfig) { try { - config = newConfig; - if (!config.check()) { + if (!newConfig.check()) { // Invalid configuration, not used, problems already logged. - config = null; + return; } // Prepare firmware files and add to config - setFirmwarePaths(); + setFirmwarePaths(newConfig); // Obtain more context data from template - var tplData = dataFromTemplate(); + var tplData = dataFromTemplate(newConfig); + initialConfig = newConfig; + + // Configure swtpmDefinition = Optional.ofNullable(tplData.get(SWTPM)) .map(d -> new CommandDefinition(SWTPM, d)).orElse(null); logger.finest(() -> swtpmDefinition.toString()); @@ -352,21 +360,19 @@ public class Runner extends Component { logger.finest(() -> cloudInitImgDefinition.toString()); // Forward some values to child components - qemuMonitor.configure(config.monitorSocket, - config.vm.powerdownTimeout); + qemuMonitor.configure(initialConfig.monitorSocket, + initialConfig.vm.powerdownTimeout); configureAgentClient(guestAgentClient, "guest-agent-socket"); configureAgentClient(vmopAgentClient, "vmop-agent-socket"); } catch (IllegalArgumentException | IOException | TemplateException e) { logger.log(Level.SEVERE, e, () -> "Invalid configuration: " + e.getMessage()); - // Don't use default configuration - config = null; } } @SuppressWarnings({ "PMD.CognitiveComplexity", "PMD.DataflowAnomalyAnalysis" }) - private void setFirmwarePaths() throws IOException { + private void setFirmwarePaths(Configuration config) throws IOException { JsonNode firmware = defaults.path("firmware").path(config.vm.firmware); // Get file for firmware ROM JsonNode codePaths = firmware.path("rom"); @@ -396,7 +402,7 @@ public class Runner extends Component { } } - private JsonNode dataFromTemplate() + private JsonNode dataFromTemplate(Configuration config) throws IOException, TemplateNotFoundException, MalformedTemplateNameException, ParseException, TemplateException, JsonProcessingException, JsonMappingException { @@ -442,7 +448,7 @@ public class Runner extends Component { */ @Handler(priority = 100) public void onStart(Start event) { - if (config == null) { + if (initialConfig == null) { // Missing configuration, fail event.cancel(true); fire(new Stop()); @@ -458,19 +464,19 @@ public class Runner extends Component { try { // Store process id try (var pidFile = Files.newBufferedWriter( - config.runtimeDir.resolve("runner.pid"))) { + initialConfig.runtimeDir.resolve("runner.pid"))) { pidFile.write(ProcessHandle.current().pid() + "\n"); } // Files to watch for - Files.deleteIfExists(config.swtpmSocket); - fire(new WatchFile(config.swtpmSocket)); + Files.deleteIfExists(initialConfig.swtpmSocket); + fire(new WatchFile(initialConfig.swtpmSocket)); // Helper files - var ticket = Optional.ofNullable(config.vm.display) + var ticket = Optional.ofNullable(initialConfig.vm.display) .map(d -> d.spice).map(s -> s.ticket); if (ticket.isPresent()) { - Files.write(config.runtimeDir.resolve("ticket.txt"), + Files.write(initialConfig.runtimeDir.resolve("ticket.txt"), ticket.get().getBytes()); } } catch (IOException e) { @@ -522,12 +528,12 @@ public class Runner extends Component { "Runner has been started")); // Start first process(es) qemuLatch.add(QemuPreps.Config); - if (config.vm.useTpm && swtpmDefinition != null) { + if (initialConfig.vm.useTpm && swtpmDefinition != null) { startProcess(swtpmDefinition); qemuLatch.add(QemuPreps.Tpm); } - if (config.cloudInit != null) { - generateCloudInitImg(); + if (initialConfig.cloudInit != null) { + generateCloudInitImg(initialConfig); qemuLatch.add(QemuPreps.CloudInit); } mayBeStartQemu(QemuPreps.Config); @@ -546,7 +552,7 @@ public class Runner extends Component { } } - private void generateCloudInitImg() { + private void generateCloudInitImg(Configuration config) { try { var cloudInitDir = config.dataDir.resolve("cloud-init"); cloudInitDir.toFile().mkdir(); @@ -583,7 +589,7 @@ public class Runner extends Component { private boolean startProcess(CommandDefinition toStart) { logger.info( () -> "Starting process: " + String.join(" ", toStart.command)); - fire(new StartProcess(toStart.command) + rep.fire(new StartProcess(toStart.command) .setAssociated(CommandDefinition.class, toStart)); return true; } @@ -597,7 +603,7 @@ public class Runner extends Component { @Handler public void onFileChanged(FileChanged event) { if (event.change() == Kind.CREATED - && event.path().equals(config.swtpmSocket)) { + && event.path().equals(initialConfig.swtpmSocket)) { // swtpm running, maybe start qemu mayBeStartQemu(QemuPreps.Tpm); } @@ -620,7 +626,7 @@ public class Runner extends Component { .ifPresent(procDef -> { channel.setAssociated(CommandDefinition.class, procDef); try (var pidFile = Files.newBufferedWriter( - config.runtimeDir.resolve(procDef.name + ".pid"))) { + initialConfig.runtimeDir.resolve(procDef.name + ".pid"))) { pidFile.write(channel.process().toHandle().pid() + "\n"); } catch (IOException e) { throw new UndeclaredThrowableException(e); @@ -659,7 +665,10 @@ public class Runner extends Component { */ @Handler public void onQmpConfigured(QmpConfigured event) { - rep.fire(new ConfigureQemu(config, state)); + if (pendingConfig != null) { + rep.fire(new ConfigureQemu(pendingConfig, state)); + pendingConfig = null; + } } /** @@ -791,7 +800,7 @@ public class Runner extends Component { logger.log(Level.WARNING, e, () -> "Proper shutdown failed."); } - Optional.ofNullable(config).map(c -> c.runtimeDir) + Optional.ofNullable(initialConfig).map(c -> c.runtimeDir) .ifPresent(runtimeDir -> { try { Files.walk(runtimeDir).sorted(Comparator.reverseOrder())