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