From b4cb3b8694b3bbf09580742ceb20b0cb7602fe20 Mon Sep 17 00:00:00 2001 From: "Michael N. Lipp" Date: Thu, 27 Feb 2025 14:46:13 +0100 Subject: [PATCH] Control login. --- .../runner/qemu/DisplayController.java | 66 +++++++++++++- .../runner/qemu/GuestAgentClient.java | 17 ++-- .../vmoperator/runner/qemu/QemuConnector.java | 20 ++++- .../vmoperator/runner/qemu/QemuMonitor.java | 16 ++-- .../runner/qemu/VmopAgentClient.java | 89 ++++++++++++++++++- .../runner/qemu/events/VmopAgentLogIn.java | 45 ++++++++++ .../runner/qemu/events/VmopAgentLogOut.java | 27 ++++++ .../runner/qemu/events/VmopAgentLoggedIn.java | 49 ++++++++++ .../qemu/events/VmopAgentLoggedOut.java | 49 ++++++++++ 9 files changed, 349 insertions(+), 29 deletions(-) create mode 100644 org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLogIn.java create mode 100644 org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLogOut.java create mode 100644 org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLoggedIn.java create mode 100644 org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLoggedOut.java 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 7dee0a2..a2a6bc9 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 @@ -33,6 +33,10 @@ 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.RunnerStateChange.RunState; +import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentConnected; +import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLogIn; +import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLogOut; +import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLoggedIn; import org.jgrapes.core.Channel; import org.jgrapes.core.Component; import org.jgrapes.core.annotation.Handler; @@ -48,6 +52,8 @@ public class DisplayController extends Component { private String currentPassword; private String protocol; private final Path configDir; + private boolean vmopAgentConnected; + private boolean userLoginRequested; /** * Instantiates a new Display controller. @@ -63,6 +69,16 @@ public class DisplayController extends Component { fire(new WatchFile(configDir.resolve(DATA_DISPLAY_PASSWORD))); } + /** + * On vmop agent connected. + * + * @param event the event + */ + @Handler + public void onVmopAgentConnected(VmopAgentConnected event) { + vmopAgentConnected = true; + } + /** * On configure qemu. * @@ -75,7 +91,7 @@ public class DisplayController extends Component { } protocol = event.configuration().vm.display.spice != null ? "spice" : null; - updatePassword(); + configureAccess(false); } /** @@ -87,12 +103,56 @@ public class DisplayController extends Component { @SuppressWarnings("PMD.EmptyCatchBlock") public void onFileChanged(FileChanged event) { if (event.path().equals(configDir.resolve(DATA_DISPLAY_PASSWORD))) { - updatePassword(); + configureAccess(true); } } @SuppressWarnings("PMD.DataflowAnomalyAnalysis") - private void updatePassword() { + private void configureAccess(boolean passwordChange) { + var userLoginConfigured = readFromFile(DATA_DISPLAY_LOGIN) + .map(Boolean::parseBoolean).orElse(false); + if (!userLoginConfigured) { + // Check if it was configured before and there may thus be an + // active auto login + if (userLoginRequested && vmopAgentConnected) { + // Make sure to log out + fire(new VmopAgentLogOut()); + } + userLoginRequested = false; + configurePassword(); + return; + } + + // With user login configured, we have to make sure that the + // user is logged in before we set the password and thus allow + // access to the display. + userLoginRequested = true; + if (!vmopAgentConnected) { + if (passwordChange) { + logger.warning(() -> "Request for user login before " + + "VM operator agent has connected"); + } + return; + } + + var user = readFromFile(DATA_DISPLAY_USER); + if (user.isEmpty()) { + logger.warning(() -> "Login requested, but no user configured"); + } + fire(new VmopAgentLogIn(user.get()).setAssociated(this, user.get())); + } + + /** + * On vmop agent logged in. + * + * @param event the event + */ + @Handler + public void onVmopAgentLoggedIn(VmopAgentLoggedIn event) { + configurePassword(); + } + + private void configurePassword() { if (protocol == null) { return; } 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 2e5e059..880ca58 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 @@ -88,10 +88,12 @@ public class GuestAgentClient extends AgentConnector { * On guest agent command. * * @param event the event + * @throws IOException */ @Handler @SuppressWarnings("PMD.AvoidSynchronizedStatement") - public void onGuestAgentCommand(GuestAgentCommand event) { + public void onGuestAgentCommand(GuestAgentCommand event) + throws IOException { if (qemuChannel() == null) { return; } @@ -106,15 +108,10 @@ public class GuestAgentClient extends AgentConnector { return; } synchronized (executing) { - writer().ifPresent(writer -> { - try { - executing.add(command); - writer.append(asText).append('\n').flush(); - } catch (IOException e) { - // Cannot happen, but... - logger.log(Level.WARNING, e, e::getMessage); - } - }); + if (writer().isPresent()) { + executing.add(command); + sendCommand(asText); + } } } } 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 143cfc2..2e94c14 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 @@ -47,8 +47,8 @@ import org.jgrapes.util.events.WatchFile; /** * A component that handles the communication with QEMU over a socket. * - * If the log level for this class is set to fine, the messages - * exchanged on the socket are logged. + * Derived classes should log the messages exchanged on the socket + * if the log level is set to fine. */ public abstract class QemuConnector extends Component { @@ -174,6 +174,22 @@ public abstract class QemuConnector extends Component { return qemuChannel().flatMap(c -> c.associated(Writer.class)); } + /** + * Send the given command to QEMU. A newline is appended to the + * command automatically. + * + * @param command the command + * @return true, if successful + * @throws IOException Signals that an I/O exception has occurred. + */ + protected boolean sendCommand(String command) throws IOException { + if (writer().isEmpty()) { + return false; + } + writer().get().append(command).append('\n').flush(); + return true; + } + /** * Called when the connector has been connected to the socket. */ 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 000a3bf..1de8f60 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 @@ -155,11 +155,12 @@ public class QemuMonitor extends QemuConnector { * On monitor command. * * @param event the event + * @throws IOException */ @Handler @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition", "PMD.AvoidSynchronizedStatement" }) - public void onExecQmpCommand(MonitorCommand event) { + public void onExecQmpCommand(MonitorCommand event) throws IOException { var command = event.command(); logger.fine(() -> "monitor(out): " + command.toString()); String asText; @@ -171,15 +172,10 @@ public class QemuMonitor extends QemuConnector { return; } synchronized (executing) { - writer().ifPresent(writer -> { - try { - executing.add(command); - writer.append(asText).append('\n').flush(); - } catch (IOException e) { - // Cannot happen, but... - logger.log(Level.WARNING, e, e::getMessage); - } - }); + if (writer().isPresent()) { + executing.add(command); + sendCommand(asText); + } } } diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/VmopAgentClient.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/VmopAgentClient.java index 89fdaa2..f50d397 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/VmopAgentClient.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/VmopAgentClient.java @@ -19,8 +19,16 @@ package org.jdrupes.vmoperator.runner.qemu; import java.io.IOException; +import java.util.Deque; +import java.util.concurrent.ConcurrentLinkedDeque; import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentConnected; +import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLogIn; +import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLogOut; +import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLoggedIn; +import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLoggedOut; import org.jgrapes.core.Channel; +import org.jgrapes.core.Event; +import org.jgrapes.core.annotation.Handler; /** * A component that handles the communication over the vmop agent @@ -31,6 +39,8 @@ import org.jgrapes.core.Channel; */ public class VmopAgentClient extends AgentConnector { + private final Deque> executing = new ConcurrentLinkedDeque<>(); + /** * Instantiates a new VM operator agent client. * @@ -41,11 +51,82 @@ public class VmopAgentClient extends AgentConnector { super(componentChannel); } - @Override - protected void processInput(String line) throws IOException { - if (line.startsWith("220 ")) { - rep().fire(new VmopAgentConnected()); + /** + * On vmop agent login. + * + * @param event the event + * @throws IOException Signals that an I/O exception has occurred. + */ + @Handler + public void onVmopAgentLogIn(VmopAgentLogIn event) throws IOException { + logger.fine(() -> "vmop agent(out): login " + event.user()); + if (writer().isPresent()) { + executing.add(event); + sendCommand("login " + event.user()); } } + /** + * On vmop agent logout. + * + * @param event the event + * @throws IOException Signals that an I/O exception has occurred. + */ + @Handler + public void onVmopAgentLogout(VmopAgentLogOut event) throws IOException { + logger.fine(() -> "vmop agent(out): logout"); + if (writer().isPresent()) { + executing.add(event); + sendCommand("logout"); + } + } + + @Override + @SuppressWarnings({ "PMD.UnnecessaryReturn", + "PMD.AvoidLiteralsInIfCondition" }) + protected void processInput(String line) throws IOException { + logger.fine(() -> "vmop agent(in): " + line); + + // Check validity + if (line.isEmpty() || !Character.isDigit(line.charAt(0))) { + logger.warning(() -> "Illegal response: " + line); + return; + } + + // Check positive responses + if (line.startsWith("220 ")) { + rep().fire(new VmopAgentConnected()); + return; + } + if (line.startsWith("201 ")) { + Event cmd = executing.pop(); + if (cmd instanceof VmopAgentLogIn login) { + rep().fire(new VmopAgentLoggedIn(login)); + } else { + logger.severe(() -> "Response " + line + + " does not match executing command " + cmd); + } + return; + } + if (line.startsWith("202 ")) { + Event cmd = executing.pop(); + if (cmd instanceof VmopAgentLogOut logout) { + rep().fire(new VmopAgentLoggedOut(logout)); + } else { + logger.severe(() -> "Response " + line + + "does not match executing command " + cmd); + } + return; + } + + // Ignore unhandled continuations + if (line.charAt(0) == '1') { + return; + } + + // Error + logger.warning(() -> "Error response: " + line); + executing.pop(); + } + } diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLogIn.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLogIn.java new file mode 100644 index 0000000..96db884 --- /dev/null +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLogIn.java @@ -0,0 +1,45 @@ +/* + * VM-Operator + * Copyright (C) 2025 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; + +import org.jgrapes.core.Event; + +/** + * Sends the login command to the VM operator agent. + */ +public class VmopAgentLogIn extends Event { + + private final String user; + + /** + * Instantiates a new vmop agent logout. + */ + public VmopAgentLogIn(String user) { + this.user = user; + } + + /** + * Returns the user. + * + * @return the user + */ + public String user() { + return user; + } +} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLogOut.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLogOut.java new file mode 100644 index 0000000..1502200 --- /dev/null +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLogOut.java @@ -0,0 +1,27 @@ +/* + * VM-Operator + * Copyright (C) 2025 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; + +import org.jgrapes.core.Event; + +/** + * Sends the logout command to the VM operator agent. + */ +public class VmopAgentLogOut extends Event { +} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLoggedIn.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLoggedIn.java new file mode 100644 index 0000000..f59ed71 --- /dev/null +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLoggedIn.java @@ -0,0 +1,49 @@ +/* + * VM-Operator + * Copyright (C) 2025 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; + +import org.jgrapes.core.Event; + +/** + * Signals that the logout command has been processes by the + * VM operator agent. + */ +public class VmopAgentLoggedIn extends Event { + + private final VmopAgentLogIn triggering; + + /** + * Instantiates a new vmop agent logged in. + * + * @param triggeringEvent the triggering event + */ + public VmopAgentLoggedIn(VmopAgentLogIn triggeringEvent) { + this.triggering = triggeringEvent; + } + + /** + * Gets the triggering event. + * + * @return the triggering + */ + public VmopAgentLogIn triggering() { + return triggering; + } + +} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLoggedOut.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLoggedOut.java new file mode 100644 index 0000000..5f60e00 --- /dev/null +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLoggedOut.java @@ -0,0 +1,49 @@ +/* + * VM-Operator + * Copyright (C) 2025 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; + +import org.jgrapes.core.Event; + +/** + * Signals that the logout command has been processes by the + * VM operator agent. + */ +public class VmopAgentLoggedOut extends Event { + + private final VmopAgentLogOut triggering; + + /** + * Instantiates a new vmop agent logged out. + * + * @param triggeringEvent the triggering event + */ + public VmopAgentLoggedOut(VmopAgentLogOut triggeringEvent) { + this.triggering = triggeringEvent; + } + + /** + * Gets the triggering event. + * + * @return the triggering + */ + public VmopAgentLogOut triggering() { + return triggering; + } + +}