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", "PMD.TooManyFields" })
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 logged in user. */
252        public String loggedInUser;
253
254        /** The spice. */
255        public Spice spice;
256    }
257
258    /**
259     * Subsection "spice".
260     */
261    @SuppressWarnings("PMD.DataClass")
262    public static class Spice implements Dto {
263
264        /** The port. */
265        public int port = 5900;
266
267        /** The ticket. */
268        public String ticket;
269
270        /** The streaming video. */
271        public String streamingVideo;
272
273        /** The usb redirects. */
274        public int usbRedirects = 2;
275    }
276
277    /**
278     * Check configuration.
279     *
280     * @return true, if successful
281     */
282    public boolean check() {
283        if (vm == null || vm.name == null) {
284            logger.severe(() -> "Configuration is missing mandatory entries.");
285            return false;
286        }
287        if (!checkRuntimeDir() || !checkDataDir() || !checkUuid()) {
288            return false;
289        }
290
291        // Adjust max cpus if necessary
292        if (vm.currentCpus > vm.maximumCpus) {
293            vm.maximumCpus = vm.currentCpus;
294        }
295
296        checkDrives();
297        checkCloudInit();
298
299        return true;
300    }
301
302    @SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
303    private void checkDrives() {
304        for (Drive drive : vm.drives) {
305            if (drive.file != null || drive.device != null
306                || "ide-cd".equals(drive.type)) {
307                continue;
308            }
309            if (drive.resource == null) {
310                logger.severe(
311                    () -> "Drive configuration is missing its resource.");
312
313            }
314            if (Files.isRegularFile(Path.of(drive.resource))) {
315                drive.file = drive.resource;
316            } else {
317                drive.device = drive.resource;
318            }
319        }
320    }
321
322    @SuppressWarnings("PMD.AvoidDeeplyNestedIfStmts")
323    private boolean checkRuntimeDir() {
324        // Runtime directory (sockets etc.)
325        if (runtimeDir == null) {
326            var appDir = FsdUtils.runtimeDir(APP_NAME.replace("-", ""));
327            if (!Files.exists(appDir) && appDir.toFile().mkdirs()) {
328                try {
329                    // When appDir is derived from XDG_RUNTIME_DIR
330                    // the latter should already have these permissions,
331                    // but let's be on the safe side.
332                    Files.setPosixFilePermissions(appDir,
333                        Set.of(PosixFilePermission.OWNER_READ,
334                            PosixFilePermission.OWNER_WRITE,
335                            PosixFilePermission.OWNER_EXECUTE));
336                } catch (IOException e) {
337                    logger.warning(() -> String.format(
338                        "Cannot set permissions rwx------ on \"%s\".",
339                        runtimeDir));
340                }
341            }
342            runtimeDir = FsdUtils.runtimeDir(APP_NAME.replace("-", ""))
343                .resolve(vm.name);
344            runtimeDir.toFile().mkdir();
345            swtpmSocket = runtimeDir.resolve("swtpm-sock");
346            monitorSocket = runtimeDir.resolve("monitor.sock");
347        }
348        if (!Files.isDirectory(runtimeDir) || !Files.isWritable(runtimeDir)) {
349            logger.severe(() -> String.format(
350                "Configured runtime directory \"%s\""
351                    + " does not exist or isn't writable.",
352                runtimeDir));
353            return false;
354        }
355        return true;
356    }
357
358    @SuppressWarnings("PMD.AvoidDeeplyNestedIfStmts")
359    private boolean checkDataDir() {
360        // Data directory
361        if (dataDir == null) {
362            dataDir
363                = FsdUtils.dataHome(APP_NAME.replace("-", "")).resolve(vm.name);
364        }
365        if (!Files.exists(dataDir)) {
366            dataDir.toFile().mkdirs();
367        }
368        if (!Files.isDirectory(dataDir) || !Files.isWritable(dataDir)) {
369            logger.severe(() -> String.format(
370                "Configured data directory \"%s\""
371                    + " does not exist or isn't writable.",
372                dataDir));
373            return false;
374        }
375        return true;
376    }
377
378    private boolean checkUuid() {
379        // Explicitly configured uuid takes precedence.
380        if (vm.uuid != null) {
381            return true;
382        }
383
384        // Try to read stored uuid.
385        Path uuidPath = dataDir.resolve("uuid.txt");
386        if (Files.isReadable(uuidPath)) {
387            try {
388                var stored
389                    = Files.lines(uuidPath, StandardCharsets.UTF_8).findFirst();
390                if (stored.isPresent()) {
391                    vm.uuid = stored.get();
392                    return true;
393                }
394            } catch (IOException e) {
395                logger.log(Level.WARNING, e,
396                    () -> "Stored uuid cannot be read: " + e.getMessage());
397            }
398        }
399
400        // Generate new uuid
401        vm.uuid = UUID.randomUUID().toString();
402        try {
403            Files.writeString(uuidPath, vm.uuid + "\n");
404        } catch (IOException e) {
405            logger.log(Level.WARNING, e,
406                () -> "Cannot store uuid: " + e.getMessage());
407        }
408
409        return true;
410    }
411
412    private void checkCloudInit() {
413        if (cloudInit == null) {
414            return;
415        }
416
417        // Provide default for instance-id
418        if (cloudInit.metaData == null) {
419            cloudInit.metaData = new HashMap<>();
420        }
421        if (!cloudInit.metaData.containsKey(CI_INSTANCE_ID)) {
422            cloudInit.metaData.put(CI_INSTANCE_ID, "v" + asOf.getEpochSecond());
423        }
424    }
425}