001/*
002 * VM-Operator
003 * Copyright (C) 2023 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 java.io.IOException;
022import java.math.BigInteger;
023import java.nio.charset.StandardCharsets;
024import java.nio.file.Files;
025import java.nio.file.Path;
026import java.nio.file.attribute.PosixFilePermission;
027import java.time.Instant;
028import java.util.HashMap;
029import java.util.Map;
030import java.util.Set;
031import java.util.UUID;
032import java.util.logging.Level;
033import java.util.logging.Logger;
034import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
035import org.jdrupes.vmoperator.common.Convertions;
036import org.jdrupes.vmoperator.util.Dto;
037import org.jdrupes.vmoperator.util.FsdUtils;
038
039/**
040 * The configuration information from the configuration file.
041 */
042@SuppressWarnings("PMD.ExcessivePublicCount")
043public class Configuration implements Dto {
044    private static final String CI_INSTANCE_ID = "instance-id";
045
046    @SuppressWarnings("PMD.FieldNamingConventions")
047    protected final Logger logger = Logger.getLogger(getClass().getName());
048
049    /** Configuration timestamp. */
050    public Instant asOf;
051
052    /** The data dir. */
053    public Path dataDir;
054
055    /** The runtime dir. */
056    public Path runtimeDir;
057
058    /** The template. */
059    public String template;
060
061    /** The update template. */
062    public boolean updateTemplate;
063
064    /** The swtpm socket. */
065    public Path swtpmSocket;
066
067    /** The monitor socket. */
068    public Path monitorSocket;
069
070    /** The firmware rom. */
071    public Path firmwareRom;
072
073    /** The firmware vars. */
074    public Path firmwareVars;
075
076    /** The display password. */
077    public boolean hasDisplayPassword;
078
079    /** Optional cloud-init data. */
080    public CloudInit cloudInit;
081
082    /** If guest shutdown changes CRD .vm.state to "Stopped". */
083    public boolean guestShutdownStops;
084
085    /** Increments of the reset counter trigger a reset of the VM. */
086    public Integer resetCounter;
087
088    /** The vm. */
089    @SuppressWarnings("PMD.ShortVariable")
090    public Vm vm;
091
092    /**
093     * Subsection "cloud-init".
094     */
095    public static class CloudInit implements Dto {
096
097        /** The meta data. */
098        @SuppressWarnings("PMD.UseConcurrentHashMap")
099        public Map<String, Object> metaData;
100
101        /** The user data. */
102        @SuppressWarnings("PMD.UseConcurrentHashMap")
103        public Map<String, Object> userData;
104
105        /** The network config. */
106        @SuppressWarnings("PMD.UseConcurrentHashMap")
107        public Map<String, Object> networkConfig;
108    }
109
110    /**
111     * Subsection "vm".
112     */
113    @SuppressWarnings({ "PMD.ShortClassName", "PMD.TooManyFields",
114        "PMD.DataClass", "PMD.AvoidDuplicateLiterals" })
115    public static class Vm implements Dto {
116
117        /** The name. */
118        public String name;
119
120        /** The uuid. */
121        public String uuid;
122
123        /** The use tpm. */
124        public boolean useTpm;
125
126        /** The boot menu. */
127        public boolean bootMenu;
128
129        /** The firmware. */
130        public String firmware = "uefi";
131
132        /** The maximum ram. */
133        public BigInteger maximumRam;
134
135        /** The current ram. */
136        public BigInteger currentRam;
137
138        /** The cpu model. */
139        public String cpuModel = "host";
140
141        /** The maximum cpus. */
142        public int maximumCpus = 1;
143
144        /** The current cpus. */
145        public int currentCpus = 1;
146
147        /** The cpu sockets. */
148        public int sockets;
149
150        /** The dies per socket. */
151        public int diesPerSocket;
152
153        /** The cores per die. */
154        public int coresPerDie;
155
156        /** The threads per core. */
157        public int threadsPerCore;
158
159        /** The accelerator. */
160        public String accelerator = "kvm";
161
162        /** The rtc base. */
163        public String rtcBase = "utc";
164
165        /** The rtc clock. */
166        public String rtcClock = "rt";
167
168        /** The powerdown timeout. */
169        public int powerdownTimeout = 900;
170
171        /** The network. */
172        public Network[] network = { new Network() };
173
174        /** The drives. */
175        public Drive[] drives = new Drive[0];
176
177        /** The display. */
178        public Display display;
179
180        /**
181         * Convert value from JSON parser.
182         *
183         * @param value the new maximum ram
184         */
185        public void setMaximumRam(String value) {
186            maximumRam = Convertions.parseMemory(value);
187        }
188
189        /**
190         * Convert value from JSON parser.
191         *
192         * @param value the new current ram
193         */
194        public void setCurrentRam(String value) {
195            currentRam = Convertions.parseMemory(value);
196        }
197    }
198
199    /**
200     * Subsection "network".
201     */
202    @SuppressWarnings("PMD.DataClass")
203    public static class Network implements Dto {
204
205        /** The type. */
206        public String type = "tap";
207
208        /** The bridge. */
209        public String bridge;
210
211        /** The device. */
212        public String device = "virtio-net";
213
214        /** The mac. */
215        public String mac;
216
217        /** The net. */
218        public String net;
219    }
220
221    /**
222     * Subsection "drive".
223     */
224    @SuppressWarnings("PMD.DataClass")
225    public static class Drive implements Dto {
226
227        /** The type. */
228        public String type;
229
230        /** The bootindex. */
231        public Integer bootindex;
232
233        /** The device. */
234        public String device;
235
236        /** The file. */
237        public String file;
238
239        /** The resource. */
240        public String resource;
241    }
242
243    /**
244     * The Class Display.
245     */
246    public static class Display implements Dto {
247
248        /** The number of outputs. */
249        public int outputs = 1;
250
251        /** The spice. */
252        public Spice spice;
253    }
254
255    /**
256     * Subsection "spice".
257     */
258    @SuppressWarnings("PMD.DataClass")
259    public static class Spice implements Dto {
260
261        /** The port. */
262        public int port = 5900;
263
264        /** The ticket. */
265        public String ticket;
266
267        /** The streaming video. */
268        public String streamingVideo;
269
270        /** The usb redirects. */
271        public int usbRedirects = 2;
272    }
273
274    /**
275     * Check configuration.
276     *
277     * @return true, if successful
278     */
279    public boolean check() {
280        if (vm == null || vm.name == null) {
281            logger.severe(() -> "Configuration is missing mandatory entries.");
282            return false;
283        }
284        if (!checkRuntimeDir() || !checkDataDir() || !checkUuid()) {
285            return false;
286        }
287
288        // Adjust max cpus if necessary
289        if (vm.currentCpus > vm.maximumCpus) {
290            vm.maximumCpus = vm.currentCpus;
291        }
292
293        checkDrives();
294        checkCloudInit();
295
296        return true;
297    }
298
299    @SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
300    private void checkDrives() {
301        for (Drive drive : vm.drives) {
302            if (drive.file != null || drive.device != null
303                || "ide-cd".equals(drive.type)) {
304                continue;
305            }
306            if (drive.resource == null) {
307                logger.severe(
308                    () -> "Drive configuration is missing its resource.");
309
310            }
311            if (Files.isRegularFile(Path.of(drive.resource))) {
312                drive.file = drive.resource;
313            } else {
314                drive.device = drive.resource;
315            }
316        }
317    }
318
319    @SuppressWarnings("PMD.AvoidDeeplyNestedIfStmts")
320    private boolean checkRuntimeDir() {
321        // Runtime directory (sockets etc.)
322        if (runtimeDir == null) {
323            var appDir = FsdUtils.runtimeDir(APP_NAME.replace("-", ""));
324            if (!Files.exists(appDir) && appDir.toFile().mkdirs()) {
325                try {
326                    // When appDir is derived from XDG_RUNTIME_DIR
327                    // the latter should already have these permissions,
328                    // but let's be on the safe side.
329                    Files.setPosixFilePermissions(appDir,
330                        Set.of(PosixFilePermission.OWNER_READ,
331                            PosixFilePermission.OWNER_WRITE,
332                            PosixFilePermission.OWNER_EXECUTE));
333                } catch (IOException e) {
334                    logger.warning(() -> String.format(
335                        "Cannot set permissions rwx------ on \"%s\".",
336                        runtimeDir));
337                }
338            }
339            runtimeDir = FsdUtils.runtimeDir(APP_NAME.replace("-", ""))
340                .resolve(vm.name);
341            runtimeDir.toFile().mkdir();
342            swtpmSocket = runtimeDir.resolve("swtpm-sock");
343            monitorSocket = runtimeDir.resolve("monitor.sock");
344        }
345        if (!Files.isDirectory(runtimeDir) || !Files.isWritable(runtimeDir)) {
346            logger.severe(() -> String.format(
347                "Configured runtime directory \"%s\""
348                    + " does not exist or isn't writable.",
349                runtimeDir));
350            return false;
351        }
352        return true;
353    }
354
355    @SuppressWarnings("PMD.AvoidDeeplyNestedIfStmts")
356    private boolean checkDataDir() {
357        // Data directory
358        if (dataDir == null) {
359            dataDir
360                = FsdUtils.dataHome(APP_NAME.replace("-", "")).resolve(vm.name);
361        }
362        if (!Files.exists(dataDir)) {
363            dataDir.toFile().mkdirs();
364        }
365        if (!Files.isDirectory(dataDir) || !Files.isWritable(dataDir)) {
366            logger.severe(() -> String.format(
367                "Configured data directory \"%s\""
368                    + " does not exist or isn't writable.",
369                dataDir));
370            return false;
371        }
372        return true;
373    }
374
375    private boolean checkUuid() {
376        // Explicitly configured uuid takes precedence.
377        if (vm.uuid != null) {
378            return true;
379        }
380
381        // Try to read stored uuid.
382        Path uuidPath = dataDir.resolve("uuid.txt");
383        if (Files.isReadable(uuidPath)) {
384            try {
385                var stored
386                    = Files.lines(uuidPath, StandardCharsets.UTF_8).findFirst();
387                if (stored.isPresent()) {
388                    vm.uuid = stored.get();
389                    return true;
390                }
391            } catch (IOException e) {
392                logger.log(Level.WARNING, e,
393                    () -> "Stored uuid cannot be read: " + e.getMessage());
394            }
395        }
396
397        // Generate new uuid
398        vm.uuid = UUID.randomUUID().toString();
399        try {
400            Files.writeString(uuidPath, vm.uuid + "\n");
401        } catch (IOException e) {
402            logger.log(Level.WARNING, e,
403                () -> "Cannot store uuid: " + e.getMessage());
404        }
405
406        return true;
407    }
408
409    private void checkCloudInit() {
410        if (cloudInit == null) {
411            return;
412        }
413
414        // Provide default for instance-id
415        if (cloudInit.metaData == null) {
416            cloudInit.metaData = new HashMap<>();
417        }
418        if (!cloudInit.metaData.containsKey(CI_INSTANCE_ID)) {
419            cloudInit.metaData.put(CI_INSTANCE_ID, "v" + asOf.getEpochSecond());
420        }
421    }
422}