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        "PMD.AssignmentInOperand" })
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}