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     * Sets the reset count.
080     *
081     * @param resetCount the reset count
082     * @return the vm extra data
083     */
084    public VmExtraData resetCount(long resetCount) {
085        this.resetCount = resetCount;
086        return this;
087    }
088
089    /**
090     * Returns the reset count.
091     *
092     * @return the long
093     */
094    public long resetCount() {
095        return resetCount;
096    }
097
098    /**
099     * Create a connection file.
100     *
101     * @param password the password
102     * @param preferredIpVersion the preferred IP version
103     * @param deleteConnectionFile the delete connection file
104     * @return the string
105     */
106    public String connectionFile(String password,
107            Class<?> preferredIpVersion, boolean deleteConnectionFile) {
108        var addr = displayIp(preferredIpVersion);
109        if (addr.isEmpty()) {
110            logger
111                .severe(() -> "Failed to find display IP for " + vmDef.name());
112            return null;
113        }
114        var port = vmDef.<Number> fromVm("display", "spice", "port")
115            .map(Number::longValue);
116        if (port.isEmpty()) {
117            logger
118                .severe(() -> "No port defined for display of " + vmDef.name());
119            return null;
120        }
121        StringBuffer data = new StringBuffer(100)
122            .append("[virt-viewer]\ntype=spice\nhost=")
123            .append(addr.get().getHostAddress()).append("\nport=")
124            .append(port.get().toString())
125            .append('\n');
126        if (password != null) {
127            data.append("password=").append(password).append('\n');
128        }
129        vmDef.<String> fromVm("display", "spice", "proxyUrl")
130            .ifPresent(u -> {
131                if (!Strings.isNullOrEmpty(u)) {
132                    data.append("proxy=").append(u).append('\n');
133                }
134            });
135        if (deleteConnectionFile) {
136            data.append("delete-this-file=1\n");
137        }
138        return data.toString();
139    }
140
141    private Optional<InetAddress> displayIp(Class<?> preferredIpVersion) {
142        Optional<String> server = vmDef.fromVm("display", "spice", "server");
143        if (server.isPresent()) {
144            var srv = server.get();
145            try {
146                var addr = InetAddress.getByName(srv);
147                logger.fine(() -> "Using IP address from CRD for "
148                    + vmDef.metadata().getName() + ": " + addr);
149                return Optional.of(addr);
150            } catch (UnknownHostException e) {
151                logger.log(Level.SEVERE, e, () -> "Invalid server address "
152                    + srv + ": " + e.getMessage());
153                return Optional.empty();
154            }
155        }
156        var addrs = nodeAddresses.stream().map(a -> {
157            try {
158                return InetAddress.getByName(a);
159            } catch (UnknownHostException e) {
160                logger.warning(() -> "Invalid IP address: " + a);
161                return null;
162            }
163        }).filter(Objects::nonNull).toList();
164        logger.fine(
165            () -> "Known IP addresses for " + vmDef.name() + ": " + addrs);
166        return addrs.stream()
167            .filter(a -> preferredIpVersion.isAssignableFrom(a.getClass()))
168            .findFirst().or(() -> addrs.stream().findFirst());
169    }
170
171}