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