Intermediate state.
This commit is contained in:
parent
29bc6f539c
commit
68a688c4ce
7 changed files with 83 additions and 53 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ public class GuestAgentClient extends AgentConnector {
|
|||
*/
|
||||
@Override
|
||||
protected void agentConnected() {
|
||||
fire(new GuestAgentCommand(new QmpGuestGetOsinfo()));
|
||||
rep().fire(new GuestAgentCommand(new QmpGuestGetOsinfo()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue