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}