001/*
002 * VM-Operator
003 * Copyright (C) 2024 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.google.gson.JsonObject;
022import io.kubernetes.client.apimachinery.GroupVersionKind;
023import io.kubernetes.client.openapi.ApiException;
024import io.kubernetes.client.openapi.models.EventsV1Event;
025import java.io.IOException;
026import java.util.logging.Level;
027import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
028import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
029import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
030import org.jdrupes.vmoperator.common.K8s;
031import org.jdrupes.vmoperator.common.K8sClient;
032import org.jdrupes.vmoperator.common.VmDefinitionStub;
033import org.jdrupes.vmoperator.runner.qemu.events.Exit;
034import org.jdrupes.vmoperator.runner.qemu.events.SpiceDisconnectedEvent;
035import org.jdrupes.vmoperator.runner.qemu.events.SpiceInitializedEvent;
036import org.jgrapes.core.Channel;
037import org.jgrapes.core.annotation.Handler;
038import org.jgrapes.core.events.Start;
039
040/**
041 * A (sub)component that updates the console status in the CR status.
042 * Created as child of {@link StatusUpdater}.
043 */
044@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
045public class ConsoleTracker extends VmDefUpdater {
046
047    private VmDefinitionStub vmStub;
048    private String mainChannelClientHost;
049    private long mainChannelClientPort;
050
051    /**
052     * Instantiates a new status updater.
053     *
054     * @param componentChannel the component channel
055     */
056    @SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
057    public ConsoleTracker(Channel componentChannel) {
058        super(componentChannel);
059        apiClient = (K8sClient) io.kubernetes.client.openapi.Configuration
060            .getDefaultApiClient();
061    }
062
063    /**
064     * Handle the start event.
065     *
066     * @param event the event
067     * @throws IOException 
068     * @throws ApiException 
069     */
070    @Handler
071    public void onStart(Start event) {
072        if (namespace == null) {
073            return;
074        }
075        try {
076            vmStub = VmDefinitionStub.get(apiClient,
077                new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM),
078                namespace, vmName);
079        } catch (ApiException e) {
080            logger.log(Level.SEVERE, e,
081                () -> "Cannot access VM object, terminating.");
082            event.cancel(true);
083            fire(new Exit(1));
084        }
085    }
086
087    /**
088     * On spice connected.
089     *
090     * @param event the event
091     * @throws ApiException the api exception
092     */
093    @Handler
094    @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition",
095        "PMD.AvoidDuplicateLiterals" })
096    public void onSpiceInitialized(SpiceInitializedEvent event)
097            throws ApiException {
098        if (vmStub == null) {
099            return;
100        }
101
102        // Only process connections using main channel.
103        if (event.channelType() != 1) {
104            return;
105        }
106        mainChannelClientHost = event.clientHost();
107        mainChannelClientPort = event.clientPort();
108        vmStub.updateStatus(from -> {
109            JsonObject status = from.status();
110            status.addProperty("consoleClient", event.clientHost());
111            updateCondition(from, status, "ConsoleConnected", true, "Connected",
112                "Connection from " + event.clientHost());
113            return status;
114        });
115
116        // Log event
117        var evt = new EventsV1Event()
118            .reportingController(VM_OP_GROUP + "/" + APP_NAME)
119            .action("ConsoleConnectionUpdate")
120            .reason("Connection from " + event.clientHost());
121        K8s.createEvent(apiClient, vmStub.model().get(), evt);
122    }
123
124    /**
125     * On spice disconnected.
126     *
127     * @param event the event
128     * @throws ApiException the api exception
129     */
130    @Handler
131    @SuppressWarnings("PMD.AvoidDuplicateLiterals")
132    public void onSpiceDisconnected(SpiceDisconnectedEvent event)
133            throws ApiException {
134        if (vmStub == null) {
135            return;
136        }
137
138        // Only process disconnects from main channel.
139        if (!event.clientHost().equals(mainChannelClientHost)
140            || event.clientPort() != mainChannelClientPort) {
141            return;
142        }
143        vmStub.updateStatus(from -> {
144            JsonObject status = from.status();
145            status.addProperty("consoleClient", "");
146            updateCondition(from, status, "ConsoleConnected", false,
147                "Disconnected", event.clientHost() + " has disconnected");
148            return status;
149        });
150
151        // Log event
152        var evt = new EventsV1Event()
153            .reportingController(VM_OP_GROUP + "/" + APP_NAME)
154            .action("ConsoleConnectionUpdate")
155            .reason("Disconnected from " + event.clientHost());
156        K8s.createEvent(apiClient, vmStub.model().get(), evt);
157    }
158}