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.common;
020
021import java.time.Duration;
022import java.time.Instant;
023import java.util.Collection;
024import java.util.Collections;
025import java.util.HashSet;
026import java.util.List;
027import java.util.Set;
028import java.util.function.Function;
029import java.util.stream.Collectors;
030import org.jdrupes.vmoperator.common.VmDefinition.Grant;
031import org.jdrupes.vmoperator.common.VmDefinition.Permission;
032import org.jdrupes.vmoperator.util.DataPath;
033
034/**
035 * Represents a VM pool.
036 */
037@SuppressWarnings({ "PMD.DataClass" })
038public class VmPool {
039
040    private String name;
041    private String retention;
042    private boolean defined;
043    private List<Grant> permissions = Collections.emptyList();
044    private final Set<String> vms
045        = Collections.synchronizedSet(new HashSet<>());
046
047    /**
048     * Instantiates a new vm pool.
049     *
050     * @param name the name
051     */
052    public VmPool(String name) {
053        this.name = name;
054    }
055
056    /**
057     * Returns the name.
058     *
059     * @return the name
060     */
061    public String name() {
062        return name;
063    }
064
065    /**
066     * Sets the name.
067     *
068     * @param name the name to set
069     */
070    public void setName(String name) {
071        this.name = name;
072    }
073
074    /**
075     * Checks if is defined.
076     *
077     * @return the result
078     */
079    public boolean isDefined() {
080        return defined;
081    }
082
083    /**
084     * Sets if is.
085     *
086     * @param defined the defined to set
087     */
088    public void setDefined(boolean defined) {
089        this.defined = defined;
090    }
091
092    /**
093     * Gets the retention.
094     *
095     * @return the retention
096     */
097    public String retention() {
098        return retention;
099    }
100
101    /**
102     * Sets the retention.
103     *
104     * @param retention the retention to set
105     */
106    public void setRetention(String retention) {
107        this.retention = retention;
108    }
109
110    /**
111     * Permissions granted for a VM from the pool.
112     *
113     * @return the permissions
114     */
115    public List<Grant> permissions() {
116        return permissions;
117    }
118
119    /**
120     * Sets the permissions.
121     *
122     * @param permissions the permissions to set
123     */
124    public void setPermissions(List<Grant> permissions) {
125        this.permissions = permissions;
126    }
127
128    /**
129     * Returns the VM names.
130     *
131     * @return the vms
132     */
133    public Set<String> vms() {
134        return vms;
135    }
136
137    /**
138     * Collect all permissions for the given user with the given roles.
139     *
140     * @param user the user
141     * @param roles the roles
142     * @return the sets the
143     */
144    public Set<Permission> permissionsFor(String user,
145            Collection<String> roles) {
146        return permissions.stream()
147            .filter(g -> DataPath.get(g, "user").map(u -> u.equals(user))
148                .orElse(false)
149                || DataPath.get(g, "role").map(roles::contains).orElse(false))
150            .map(g -> DataPath.<Set<Permission>> get(g, "may")
151                .orElse(Collections.emptySet()).stream())
152            .flatMap(Function.identity()).collect(Collectors.toSet());
153    }
154
155    /**
156     * Checks if the given VM belongs to the pool and is not in use.
157     *
158     * @param vmDef the vm def
159     * @return true, if is assignable
160     */
161    @SuppressWarnings("PMD.SimplifyBooleanReturns")
162    public boolean isAssignable(VmDefinition vmDef) {
163        // Check if the VM is in the pool
164        if (!vmDef.pools().contains(name)) {
165            return false;
166        }
167
168        // Check if the VM is not in use
169        if (vmDef.consoleConnected()) {
170            return false;
171        }
172
173        // If not assigned, it's usable
174        if (vmDef.assignedTo().isEmpty()) {
175            return true;
176        }
177
178        // Check if it is to be retained
179        if (vmDef.assignmentLastUsed()
180            .map(this::retainUntil)
181            .map(ru -> Instant.now().isBefore(ru)).orElse(false)) {
182            return false;
183        }
184
185        // Additional check in case lastUsed has not been updated
186        // by PoolMonitor#onVmDefChanged() yet ("race condition")
187        if (vmDef.condition("ConsoleConnected")
188            .map(cc -> cc.getLastTransitionTime().toInstant())
189            .map(this::retainUntil)
190            .map(ru -> Instant.now().isBefore(ru)).orElse(false)) {
191            return false;
192        }
193        return true;
194    }
195
196    /**
197     * Return the instant until which an assignment should be retained.
198     *
199     * @param lastUsed the last used
200     * @return the instant
201     */
202    public Instant retainUntil(Instant lastUsed) {
203        if (retention.startsWith("P")) {
204            return lastUsed.plus(Duration.parse(retention));
205        }
206        return Instant.parse(retention);
207    }
208
209    /**
210     * To string.
211     *
212     * @return the string
213     */
214    @Override
215    @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition",
216        "PMD.AvoidSynchronizedStatement" })
217    public String toString() {
218        StringBuilder builder = new StringBuilder(50);
219        builder.append("VmPool [name=").append(name).append(", permissions=")
220            .append(permissions).append(", vms=");
221        if (vms.size() <= 3) {
222            builder.append(vms);
223        } else {
224            synchronized (vms) {
225                builder.append('[').append(vms.stream().limit(3)
226                    .map(s -> s + ",").collect(Collectors.joining()))
227                    .append("...]");
228            }
229        }
230        builder.append(']');
231        return builder.toString();
232    }
233}