001/* 002 * VM-Operator 003 * Copyright (C) 2023 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 com.fasterxml.jackson.databind.node.ObjectNode; 022import java.util.HashSet; 023import java.util.LinkedList; 024import java.util.List; 025import java.util.Objects; 026import java.util.Optional; 027import java.util.Set; 028import org.jdrupes.vmoperator.runner.qemu.commands.QmpAddCpu; 029import org.jdrupes.vmoperator.runner.qemu.commands.QmpDelCpu; 030import org.jdrupes.vmoperator.runner.qemu.commands.QmpQueryHotpluggableCpus; 031import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu; 032import org.jdrupes.vmoperator.runner.qemu.events.CpuAdded; 033import org.jdrupes.vmoperator.runner.qemu.events.CpuDeleted; 034import org.jdrupes.vmoperator.runner.qemu.events.HotpluggableCpuStatus; 035import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand; 036import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState; 037import org.jgrapes.core.Channel; 038import org.jgrapes.core.Component; 039import org.jgrapes.core.annotation.Handler; 040 041/** 042 * The Class CpuController. 043 */ 044@SuppressWarnings("PMD.DataflowAnomalyAnalysis") 045public class CpuController extends Component { 046 047 private Integer currentCpus; 048 private Integer desiredCpus; 049 private ConfigureQemu suspendedConfigure; 050 051 /** 052 * Instantiates a new CPU controller. 053 * 054 * @param componentChannel the component channel 055 */ 056 public CpuController(Channel componentChannel) { 057 super(componentChannel); 058 } 059 060 /** 061 * On configure qemu. 062 * 063 * @param event the event 064 */ 065 @Handler 066 public void onConfigureQemu(ConfigureQemu event) { 067 if (event.runState() == RunState.TERMINATING) { 068 return; 069 } 070 Optional.ofNullable(event.configuration().vm.currentCpus) 071 .ifPresent(cpus -> { 072 if (desiredCpus != null && desiredCpus.equals(cpus)) { 073 return; 074 } 075 event.suspendHandling(); 076 suspendedConfigure = event; 077 desiredCpus = cpus; 078 fire(new MonitorCommand(new QmpQueryHotpluggableCpus())); 079 }); 080 } 081 082 /** 083 * On monitor result. 084 * 085 * @param event the result 086 */ 087 @Handler 088 public void onHotpluggableCpuStatus(HotpluggableCpuStatus event) { 089 if (!event.successful()) { 090 logger.warning(() -> "Failed to get hotpluggable CPU status " 091 + "(won't adjust number of CPUs.): " + event.errorMessage()); 092 } 093 if (desiredCpus == null) { 094 return; 095 } 096 // Process 097 currentCpus = event.usedCpus().size(); 098 int diff = currentCpus - desiredCpus; 099 if (diff == 0) { 100 return; 101 } 102 diff = addCpus(event.usedCpus(), event.unusedCpus(), diff); 103 removeCpus(event.usedCpus(), diff); 104 105 // Report result 106 fire(new MonitorCommand(new QmpQueryHotpluggableCpus())); 107 } 108 109 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 110 private int addCpus(List<ObjectNode> used, List<ObjectNode> unused, 111 int diff) { 112 Set<String> usedIds = new HashSet<>(); 113 for (var cpu : used) { 114 String qomPath = cpu.get("qom-path").asText(); 115 if (qomPath.startsWith("/machine/peripheral/cpu-")) { 116 usedIds 117 .add(qomPath.substring(qomPath.lastIndexOf('/') + 1)); 118 } 119 } 120 int nextId = 1; 121 List<ObjectNode> remaining = new LinkedList<>(unused); 122 while (diff < 0 && !remaining.isEmpty()) { 123 String id; 124 do { 125 id = "cpu-" + nextId++; 126 } while (usedIds.contains(id)); 127 fire(new MonitorCommand(new QmpAddCpu(remaining.get(0), id))); 128 remaining.remove(0); 129 diff += 1; 130 } 131 return diff; 132 } 133 134 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 135 private int removeCpus(List<ObjectNode> used, int diff) { 136 List<ObjectNode> removable = new LinkedList<>(used); 137 while (diff > 0 && !removable.isEmpty()) { 138 ObjectNode cpu = removable.remove(0); 139 String qomPath = cpu.get("qom-path").asText(); 140 if (!qomPath.startsWith("/machine/peripheral/cpu-")) { 141 continue; 142 } 143 String id = qomPath.substring(qomPath.lastIndexOf('/') + 1); 144 fire(new MonitorCommand(new QmpDelCpu(id))); 145 diff -= 1; 146 } 147 return diff; 148 } 149 150 /** 151 * On cpu added. 152 * 153 * @param event the event 154 */ 155 @Handler 156 public void onCpuAdded(CpuAdded event) { 157 currentCpus += 1; 158 checkCpus(); 159 } 160 161 /** 162 * On cpu deleted. 163 * 164 * @param event the event 165 */ 166 @Handler 167 public void onCpuDeleted(CpuDeleted event) { 168 currentCpus -= 1; 169 checkCpus(); 170 } 171 172 private void checkCpus() { 173 if (suspendedConfigure != null && desiredCpus != null 174 && Objects.equals(currentCpus, desiredCpus)) { 175 suspendedConfigure.resumeHandling(); 176 suspendedConfigure = null; 177 } 178 } 179}