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 org.jdrupes.vmoperator.common.Constants.Crd;
029import org.jdrupes.vmoperator.common.Constants.Status;
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(Crd.GROUP, "", Crd.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 = updateCondition(from, "ConsoleConnected", true,
110                "Connected", "Connection from " + event.clientHost());
111            status.addProperty(Status.CONSOLE_CLIENT, event.clientHost());
112            return status;
113        });
114
115        // Log event
116        var evt = new EventsV1Event()
117            .reportingController(Crd.GROUP + "/" + APP_NAME)
118            .action("ConsoleConnectionUpdate")
119            .reason("Connection from " + event.clientHost());
120        K8s.createEvent(apiClient, vmStub.model().get(), evt);
121    }
122
123    /**
124     * On spice disconnected.
125     *
126     * @param event the event
127     * @throws ApiException the api exception
128     */
129    @Handler
130    @SuppressWarnings("PMD.AvoidDuplicateLiterals")
131    public void onSpiceDisconnected(SpiceDisconnectedEvent event)
132            throws ApiException {
133        if (vmStub == null) {
134            return;
135        }
136
137        // Only process disconnects from main channel.
138        if (!event.clientHost().equals(mainChannelClientHost)
139            || event.clientPort() != mainChannelClientPort) {
140            return;
141        }
142        vmStub.updateStatus(from -> {
143            JsonObject status = updateCondition(from, "ConsoleConnected", false,
144                "Disconnected", event.clientHost() + " has disconnected");
145            status.addProperty(Status.CONSOLE_CLIENT, "");
146            return status;
147        });
148
149        // Log event
150        var evt = new EventsV1Event()
151            .reportingController(Crd.GROUP + "/" + APP_NAME)
152            .action("ConsoleConnectionUpdate")
153            .reason("Disconnected from " + event.clientHost());
154        K8s.createEvent(apiClient, vmStub.model().get(), evt);
155    }
156}