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 canBeUpdated; 053 private boolean vmopAgentConnected; 054 private String loggedInUser; 055 056 /** 057 * Instantiates a new Display controller. 058 * 059 * @param componentChannel the component channel 060 * @param configDir 061 */ 062 @SuppressWarnings({ "PMD.AssignmentToNonFinalStatic", 063 "PMD.ConstructorCallsOverridableMethod" }) 064 public DisplayController(Channel componentChannel, Path configDir) { 065 super(componentChannel); 066 this.configDir = configDir; 067 fire(new WatchFile(configDir.resolve(DisplaySecret.PASSWORD))); 068 } 069 070 /** 071 * On configure qemu. 072 * 073 * @param event the event 074 */ 075 @Handler 076 public void onConfigureQemu(ConfigureQemu event) { 077 if (event.runState() == RunState.TERMINATING) { 078 return; 079 } 080 protocol 081 = event.configuration().vm.display.spice != null ? "spice" : null; 082 loggedInUser = event.configuration().vm.display.loggedInUser; 083 configureLogin(); 084 if (event.runState() == RunState.STARTING) { 085 configurePassword(); 086 } 087 canBeUpdated = true; 088 } 089 090 /** 091 * On vmop agent connected. 092 * 093 * @param event the event 094 */ 095 @Handler 096 public void onVmopAgentConnected(VmopAgentConnected event) { 097 vmopAgentConnected = true; 098 configureLogin(); 099 } 100 101 private void configureLogin() { 102 if (!vmopAgentConnected) { 103 return; 104 } 105 Event<?> evt = loggedInUser != null 106 ? new VmopAgentLogIn(loggedInUser) 107 : new VmopAgentLogOut(); 108 fire(evt); 109 } 110 111 /** 112 * Watch for changes of the password file. 113 * 114 * @param event the event 115 */ 116 @Handler 117 @SuppressWarnings("PMD.EmptyCatchBlock") 118 public void onFileChanged(FileChanged event) { 119 if (event.path().equals(configDir.resolve(DisplaySecret.PASSWORD))) { 120 logger.fine(() -> "Display password updated"); 121 if (canBeUpdated) { 122 configurePassword(); 123 } 124 } 125 } 126 127 private void configurePassword() { 128 if (protocol == null) { 129 return; 130 } 131 if (setDisplayPassword()) { 132 setPasswordExpiry(); 133 } 134 } 135 136 private boolean setDisplayPassword() { 137 return readFromFile(DisplaySecret.PASSWORD).map(password -> { 138 if (Objects.equals(this.currentPassword, password)) { 139 return true; 140 } 141 this.currentPassword = password; 142 logger.fine(() -> "Updating display password"); 143 fire(new MonitorCommand( 144 new QmpSetDisplayPassword(protocol, password))); 145 return true; 146 }).orElse(false); 147 } 148 149 private void setPasswordExpiry() { 150 readFromFile(DisplaySecret.EXPIRY).ifPresent(expiry -> { 151 logger.fine(() -> "Updating expiry time to " + expiry); 152 fire( 153 new MonitorCommand(new QmpSetPasswordExpiry(protocol, expiry))); 154 }); 155 } 156 157 private Optional<String> readFromFile(String dataItem) { 158 Path path = configDir.resolve(dataItem); 159 String label = dataItem.replace('-', ' '); 160 if (path.toFile().canRead()) { 161 logger.finer(() -> "Found " + label); 162 try { 163 return Optional.ofNullable(Files.readString(path)); 164 } catch (IOException e) { 165 logger.log(Level.WARNING, e, () -> "Cannot read " + label + ": " 166 + e.getMessage()); 167 return Optional.empty(); 168 } 169 } else { 170 logger.finer(() -> "No " + label); 171 return Optional.empty(); 172 } 173 } 174}