001/* 002 * VM-Operator 003 * Copyright (C) 2023,2025 Michael N. Lipp 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <https://www.gnu.org/licenses/>. 017 */ 018 019package org.jdrupes.vmoperator.runner.qemu; 020 021import java.io.IOException; 022import java.nio.file.Files; 023import java.nio.file.Path; 024import java.util.Objects; 025import java.util.Optional; 026import java.util.logging.Level; 027import org.jdrupes.vmoperator.common.Constants.DisplaySecret; 028import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetDisplayPassword; 029import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetPasswordExpiry; 030import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu; 031import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand; 032import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState; 033import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentConnected; 034import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLogIn; 035import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLogOut; 036import org.jgrapes.core.Channel; 037import org.jgrapes.core.Component; 038import org.jgrapes.core.Event; 039import org.jgrapes.core.annotation.Handler; 040import org.jgrapes.util.events.FileChanged; 041import org.jgrapes.util.events.WatchFile; 042 043/** 044 * The Class DisplayController. 045 */ 046@SuppressWarnings("PMD.DataflowAnomalyAnalysis") 047public class DisplayController extends Component { 048 049 private String currentPassword; 050 private String protocol; 051 private final Path configDir; 052 private boolean vmopAgentConnected; 053 private String loggedInUser; 054 055 /** 056 * Instantiates a new Display controller. 057 * 058 * @param componentChannel the component channel 059 * @param configDir 060 */ 061 @SuppressWarnings({ "PMD.AssignmentToNonFinalStatic", 062 "PMD.ConstructorCallsOverridableMethod" }) 063 public DisplayController(Channel componentChannel, Path configDir) { 064 super(componentChannel); 065 this.configDir = configDir; 066 fire(new WatchFile(configDir.resolve(DisplaySecret.PASSWORD))); 067 } 068 069 /** 070 * On configure qemu. 071 * 072 * @param event the event 073 */ 074 @Handler 075 public void onConfigureQemu(ConfigureQemu event) { 076 if (event.runState() == RunState.TERMINATING) { 077 return; 078 } 079 protocol 080 = event.configuration().vm.display.spice != null ? "spice" : null; 081 loggedInUser = event.configuration().vm.display.loggedInUser; 082 configureLogin(); 083 if (event.runState() == RunState.STARTING) { 084 configurePassword(); 085 } 086 } 087 088 /** 089 * On vmop agent connected. 090 * 091 * @param event the event 092 */ 093 @Handler 094 public void onVmopAgentConnected(VmopAgentConnected event) { 095 vmopAgentConnected = true; 096 configureLogin(); 097 } 098 099 private void configureLogin() { 100 if (!vmopAgentConnected) { 101 return; 102 } 103 Event<?> evt = loggedInUser != null 104 ? new VmopAgentLogIn(loggedInUser) 105 : new VmopAgentLogOut(); 106 fire(evt); 107 } 108 109 /** 110 * Watch for changes of the password file. 111 * 112 * @param event the event 113 */ 114 @Handler 115 @SuppressWarnings("PMD.EmptyCatchBlock") 116 public void onFileChanged(FileChanged event) { 117 if (event.path().equals(configDir.resolve(DisplaySecret.PASSWORD))) { 118 configurePassword(); 119 } 120 } 121 122 private void configurePassword() { 123 if (protocol == null) { 124 return; 125 } 126 if (setDisplayPassword()) { 127 setPasswordExpiry(); 128 } 129 } 130 131 private boolean setDisplayPassword() { 132 return readFromFile(DisplaySecret.PASSWORD).map(password -> { 133 if (Objects.equals(this.currentPassword, password)) { 134 return true; 135 } 136 this.currentPassword = password; 137 logger.fine(() -> "Updating display password"); 138 fire(new MonitorCommand( 139 new QmpSetDisplayPassword(protocol, password))); 140 return true; 141 }).orElse(false); 142 } 143 144 private void setPasswordExpiry() { 145 readFromFile(DisplaySecret.EXPIRY).ifPresent(expiry -> { 146 logger.fine(() -> "Updating expiry time to " + expiry); 147 fire( 148 new MonitorCommand(new QmpSetPasswordExpiry(protocol, expiry))); 149 }); 150 } 151 152 private Optional<String> readFromFile(String dataItem) { 153 Path path = configDir.resolve(dataItem); 154 String label = dataItem.replace('-', ' '); 155 if (path.toFile().canRead()) { 156 logger.finer(() -> "Found " + label); 157 try { 158 return Optional.ofNullable(Files.readString(path)); 159 } catch (IOException e) { 160 logger.log(Level.WARNING, e, () -> "Cannot read " + label + ": " 161 + e.getMessage()); 162 return Optional.empty(); 163 } 164 } else { 165 logger.finer(() -> "No " + label); 166 return Optional.empty(); 167 } 168 } 169}