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}