001/*
002 * VM-Operator
003 * Copyright (C) 2025 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.common;
020
021import io.kubernetes.client.util.Strings;
022import java.net.InetAddress;
023import java.net.UnknownHostException;
024import java.util.Collections;
025import java.util.List;
026import java.util.Objects;
027import java.util.Optional;
028import java.util.logging.Level;
029import java.util.logging.Logger;
030
031/**
032 * Represents internally used dynamic data associated with a
033 * {@link VmDefinition}.
034 */
035public class VmExtraData {
036
037    @SuppressWarnings("PMD.FieldNamingConventions")
038    private static final Logger logger
039        = Logger.getLogger(VmExtraData.class.getName());
040
041    private final VmDefinition vmDef;
042    private String nodeName = "";
043    private List<String> nodeAddresses = Collections.emptyList();
044    private long resetCount;
045
046    /**
047     * Initializes a new instance.
048     *
049     * @param vmDef the VM definition
050     */
051    public VmExtraData(VmDefinition vmDef) {
052        this.vmDef = vmDef;
053        vmDef.extra(this);
054    }
055
056    /**
057     * Sets the node info.
058     *
059     * @param name the name
060     * @param addresses the addresses
061     * @return the VM extra data
062     */
063    public VmExtraData nodeInfo(String name, List<String> addresses) {
064        nodeName = name;
065        nodeAddresses = addresses;
066        return this;
067    }
068
069    /**
070     * Return the node name.
071     *
072     * @return the string
073     */
074    public String nodeName() {
075        return nodeName;
076    }
077
078    /**
079     * Gets the node addresses.
080     *
081     * @return the nodeAddresses
082     */
083    public List<String> nodeAddresses() {
084        return nodeAddresses;
085    }
086
087    /**
088     * Sets the reset count.
089     *
090     * @param resetCount the reset count
091     * @return the vm extra data
092     */
093    public VmExtraData resetCount(long resetCount) {
094        this.resetCount = resetCount;
095        return this;
096    }
097
098    /**
099     * Returns the reset count.
100     *
101     * @return the long
102     */
103    public long resetCount() {
104        return resetCount;
105    }
106
107    /**
108     * Create a connection file.
109     *
110     * @param password the password
111     * @param preferredIpVersion the preferred IP version
112     * @param deleteConnectionFile the delete connection file
113     * @return the string
114     */
115    public Optional<String> connectionFile(String password,
116            Class<?> preferredIpVersion, boolean deleteConnectionFile) {
117        var addr = displayIp(preferredIpVersion);
118        if (addr.isEmpty()) {
119            logger
120                .severe(() -> "Failed to find display IP for " + vmDef.name());
121            return Optional.empty();
122        }
123        var port = vmDef.<Number> fromVm("display", "spice", "port")
124            .map(Number::longValue);
125        if (port.isEmpty()) {
126            logger
127                .severe(() -> "No port defined for display of " + vmDef.name());
128            return Optional.empty();
129        }
130        StringBuffer data = new StringBuffer(100)
131            .append("[virt-viewer]\ntype=spice\nhost=")
132            .append(addr.get().getHostAddress()).append("\nport=")
133            .append(port.get().toString())
134            .append('\n');
135        if (password != null) {
136            data.append("password=").append(password).append('\n');
137        }
138        vmDef.<String> fromVm("display", "spice", "proxyUrl")
139            .ifPresent(u -> {
140                if (!Strings.isNullOrEmpty(u)) {
141                    data.append("proxy=").append(u).append('\n');
142                }
143            });
144        if (deleteConnectionFile) {
145            data.append("delete-this-file=1\n");
146        }
147        return Optional.of(data.toString());
148    }
149
150    private Optional<InetAddress> displayIp(Class<?> preferredIpVersion) {
151        Optional<String> server = vmDef.fromVm("display", "spice", "server");
152        if (server.isPresent()) {
153            var srv = server.get();
154            try {
155                var addr = InetAddress.getByName(srv);
156                logger.fine(() -> "Using IP address from CRD for "
157                    + vmDef.metadata().getName() + ": " + addr);
158                return Optional.of(addr);
159            } catch (UnknownHostException e) {
160                logger.log(Level.SEVERE, e, () -> "Invalid server address "
161                    + srv + ": " + e.getMessage());
162                return Optional.empty();
163            }
164        }
165        var addrs = nodeAddresses.stream().map(a -> {
166            try {
167                return InetAddress.getByName(a);
168            } catch (UnknownHostException e) {
169                logger.warning(() -> "Invalid IP address: " + a);
170                return null;
171            }
172        }).filter(Objects::nonNull).toList();
173        logger.fine(
174            () -> "Known IP addresses for " + vmDef.name() + ": " + addrs);
175        return addrs.stream()
176            .filter(a -> preferredIpVersion.isAssignableFrom(a.getClass()))
177            .findFirst().or(() -> addrs.stream().findFirst());
178    }
179
180}