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 */ 046public class DisplayController extends Component { 047 048 private String currentPassword; 049 private String protocol; 050 private final Path configDir; 051 private boolean canBeUpdated; 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.ConstructorCallsOverridableMethod" }) 062 public DisplayController(Channel componentChannel, Path configDir) { 063 super(componentChannel); 064 this.configDir = configDir; 065 fire(new WatchFile(configDir.resolve(DisplaySecret.PASSWORD))); 066 } 067 068 /** 069 * On configure qemu. 070 * 071 * @param event the event 072 */ 073 @Handler 074 public void onConfigureQemu(ConfigureQemu event) { 075 if (event.runState() == RunState.TERMINATING) { 076 return; 077 } 078 protocol 079 = event.configuration().vm.display.spice != null ? "spice" : null; 080 loggedInUser = event.configuration().vm.display.loggedInUser; 081 configureLogin(); 082 if (event.runState() == RunState.STARTING) { 083 configurePassword(); 084 } 085 canBeUpdated = true; 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 public void onFileChanged(FileChanged event) { 116 if (event.path().equals(configDir.resolve(DisplaySecret.PASSWORD))) { 117 logger.fine(() -> "Display password updated"); 118 if (canBeUpdated) { 119 configurePassword(); 120 } 121 } 122 } 123 124 private void configurePassword() { 125 if (protocol == null) { 126 return; 127 } 128 if (setDisplayPassword()) { 129 setPasswordExpiry(); 130 } 131 } 132 133 private boolean setDisplayPassword() { 134 return readFromFile(DisplaySecret.PASSWORD).map(password -> { 135 if (Objects.equals(this.currentPassword, password)) { 136 return true; 137 } 138 this.currentPassword = password; 139 logger.fine(() -> "Updating display password"); 140 fire(new MonitorCommand( 141 new QmpSetDisplayPassword(protocol, password))); 142 return true; 143 }).orElse(false); 144 } 145 146 private void setPasswordExpiry() { 147 readFromFile(DisplaySecret.EXPIRY).ifPresent(expiry -> { 148 logger.fine(() -> "Updating expiry time to " + expiry); 149 fire( 150 new MonitorCommand(new QmpSetPasswordExpiry(protocol, expiry))); 151 }); 152 } 153 154 private Optional<String> readFromFile(String dataItem) { 155 Path path = configDir.resolve(dataItem); 156 String label = dataItem.replace('-', ' '); 157 if (path.toFile().canRead()) { 158 logger.finer(() -> "Found " + label); 159 try { 160 return Optional.ofNullable(Files.readString(path)); 161 } catch (IOException e) { 162 logger.log(Level.WARNING, e, () -> "Cannot read " + label + ": " 163 + e.getMessage()); 164 return Optional.empty(); 165 } 166 } else { 167 logger.finer(() -> "No " + label); 168 return Optional.empty(); 169 } 170 } 171}