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