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 */
044public class ConsoleTracker extends VmDefUpdater {
045
046    private VmDefinitionStub vmStub;
047    private String mainChannelClientHost;
048    private long mainChannelClientPort;
049
050    /**
051     * Instantiates a new status updater.
052     *
053     * @param componentChannel the component channel
054     */
055    public ConsoleTracker(Channel componentChannel) {
056        super(componentChannel);
057        apiClient = (K8sClient) io.kubernetes.client.openapi.Configuration
058            .getDefaultApiClient();
059    }
060
061    /**
062     * Handle the start event.
063     *
064     * @param event the event
065     * @throws IOException 
066     * @throws ApiException 
067     */
068    @Handler
069    public void onStart(Start event) {
070        if (namespace == null) {
071            return;
072        }
073        try {
074            vmStub = VmDefinitionStub.get(apiClient,
075                new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM),
076                namespace, vmName);
077        } catch (ApiException e) {
078            logger.log(Level.SEVERE, e,
079                () -> "Cannot access VM object, terminating.");
080            event.cancel(true);
081            fire(new Exit(1));
082        }
083    }
084
085    /**
086     * On spice connected.
087     *
088     * @param event the event
089     * @throws ApiException the api exception
090     */
091    @Handler
092    @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition" })
093    public void onSpiceInitialized(SpiceInitializedEvent event)
094            throws ApiException {
095        if (vmStub == null) {
096            return;
097        }
098
099        // Only process connections using main channel.
100        if (event.channelType() != 1) {
101            return;
102        }
103        mainChannelClientHost = event.clientHost();
104        mainChannelClientPort = event.clientPort();
105        vmStub.updateStatus(from -> {
106            JsonObject status = updateCondition(from, "ConsoleConnected", true,
107                "Connected", "Connection from " + event.clientHost());
108            status.addProperty(Status.CONSOLE_CLIENT, event.clientHost());
109            return status;
110        });
111
112        // Log event
113        var evt = new EventsV1Event()
114            .reportingController(Crd.GROUP + "/" + APP_NAME)
115            .action("ConsoleConnectionUpdate")
116            .reason("Connection from " + event.clientHost());
117        K8s.createEvent(apiClient, vmStub.model().get(), evt);
118    }
119
120    /**
121     * On spice disconnected.
122     *
123     * @param event the event
124     * @throws ApiException the api exception
125     */
126    @Handler
127    public void onSpiceDisconnected(SpiceDisconnectedEvent event)
128            throws ApiException {
129        if (vmStub == null) {
130            return;
131        }
132
133        // Only process disconnects from main channel.
134        if (!event.clientHost().equals(mainChannelClientHost)
135            || event.clientPort() != mainChannelClientPort) {
136            return;
137        }
138        vmStub.updateStatus(from -> {
139            JsonObject status = updateCondition(from, "ConsoleConnected", false,
140                "Disconnected", event.clientHost() + " has disconnected");
141            status.addProperty(Status.CONSOLE_CLIENT, "");
142            return status;
143        });
144
145        // Log event
146        var evt = new EventsV1Event()
147            .reportingController(Crd.GROUP + "/" + APP_NAME)
148            .action("ConsoleConnectionUpdate")
149            .reason("Disconnected from " + event.clientHost());
150        K8s.createEvent(apiClient, vmStub.model().get(), evt);
151    }
152}