diff --git a/deploy/crds/vms-crd.yaml b/deploy/crds/vms-crd.yaml index 492deae..f1bbaf2 100644 --- a/deploy/crds/vms-crd.yaml +++ b/deploy/crds/vms-crd.yaml @@ -1019,6 +1019,11 @@ spec: - accessConsole - "*" default: [] + loggingProperties: + type: string + description: >- + Override the default logging properties for + the runner for this VM. vm: type: object description: Defines the VM. diff --git a/dev-example/config.yaml b/dev-example/config.yaml index 3f973e4..af1f3b8 100644 --- a/dev-example/config.yaml +++ b/dev-example/config.yaml @@ -17,6 +17,18 @@ metallb.universe.tf/loadBalancerIPs: 192.168.168.1 metallb.universe.tf/ip-allocated-from-pool: single-common metallb.universe.tf/allow-shared-ip: single-common + loggingProperties: | + # Defaults for namespace (VM domain) + handlers=java.util.logging.ConsoleHandler + + #org.jgrapes.level=FINE + #org.jgrapes.core.handlerTracking.level=FINER + + org.jdrupes.vmoperator.runner.qemu.level=FINEST + + java.util.logging.ConsoleHandler.level=ALL + java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter + java.util.logging.SimpleFormatter.format=%1$tb %1$td %1$tT %4$s %5$s%6$s%n "/GuiSocketServer": port: 8888 "/GuiHttpServer": diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java index 129a54d..68ca7fa 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java @@ -18,6 +18,7 @@ package org.jdrupes.vmoperator.manager; +import com.google.gson.JsonObject; import freemarker.template.Configuration; import freemarker.template.TemplateException; import io.kubernetes.client.custom.V1Patch; @@ -36,6 +37,8 @@ import org.jdrupes.vmoperator.common.K8s; import static org.jdrupes.vmoperator.manager.Constants.APP_NAME; import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME; import org.jdrupes.vmoperator.manager.events.VmChannel; +import org.jdrupes.vmoperator.util.DataPath; +import org.jdrupes.vmoperator.util.GsonPtr; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; @@ -71,10 +74,6 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; public Map reconcile(Map model, VmChannel channel) throws IOException, TemplateException, ApiException { - // Get API - DynamicKubernetesApi cmApi = new DynamicKubernetesApi("", "v1", - "configmaps", channel.client()); - // Combine template and data and parse result var fmTemplate = fmConfig.getTemplate("runnerConfig.ftl.yaml"); StringWriter out = new StringWriter(); @@ -84,8 +83,26 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; var mapDef = Dynamics.newFromYaml( new Yaml(new SafeConstructor(new LoaderOptions())), out.toString()); + // Maybe override logging.properties from reconciler configuration. + DataPath. get(model, "reconciler", "loggingProperties") + .ifPresent(props -> { + GsonPtr.to(mapDef.getRaw()).get(JsonObject.class, "data") + .get().addProperty("logging.properties", props); + }); + + // Maybe override logging.properties from VM definition. + DataPath. get(model, "cr", "spec", "loggingProperties") + .ifPresent(props -> { + GsonPtr.to(mapDef.getRaw()).get(JsonObject.class, "data") + .get().addProperty("logging.properties", props); + }); + + // Get API + DynamicKubernetesApi cmApi = new DynamicKubernetesApi("", "v1", + "configmaps", channel.client()); + // Apply and maybe force pod update - var newState = K8s.apply(cmApi, mapDef, out.toString()); + var newState = K8s.apply(cmApi, mapDef, mapDef.getRaw().toString()); maybeForceUpdate(channel.client(), newState); @SuppressWarnings("unchecked") var res = (Map) channel.client().getJSON().getGson() diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java index 32753ac..3fa2ffe 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java @@ -132,6 +132,11 @@ import org.jgrapes.util.events.ConfigurationUpdate; * ``` * This makes all VM consoles available at IP address 192.168.168.1 * with the port numbers from the VM definitions. + * + * * `loggingProperties`: If defined, specifies the default logging + * properties to be used by the runners managed by the controller. + * This property is a string that holds the content of + * a logging.properties file. */ @SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.AvoidDuplicateLiterals" }) diff --git a/org.jdrupes.vmoperator.runner.qemu/resources/org/jdrupes/vmoperator/runner/qemu/defaults.yaml b/org.jdrupes.vmoperator.runner.qemu/resources/org/jdrupes/vmoperator/runner/qemu/defaults.yaml index 9aadbf6..32fdf55 100644 --- a/org.jdrupes.vmoperator.runner.qemu/resources/org/jdrupes/vmoperator/runner/qemu/defaults.yaml +++ b/org.jdrupes.vmoperator.runner.qemu/resources/org/jdrupes/vmoperator/runner/qemu/defaults.yaml @@ -8,6 +8,9 @@ - "/usr/share/edk2/ovmf/OVMF_CODE.fd" - "/usr/share/edk2/x64/OVMF_CODE.fd" - "/usr/share/OVMF/OVMF_CODE.fd" + # Use 4M version as fallback (if smaller version not available) + - "/usr/share/edk2/ovmf-4m/OVMF_CODE.fd" + - "/usr/share/edk2/x64/OVMF_CODE.4m.fd" "vars": - "/usr/share/edk2/ovmf/OVMF_VARS.fd" - "/usr/share/edk2/x64/OVMF_VARS.fd" diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CommandDefinition.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CommandDefinition.java index 9057606..7aec209 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CommandDefinition.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CommandDefinition.java @@ -69,4 +69,9 @@ class CommandDefinition { public String name() { return name; } + + @Override + public String toString() { + return "Command " + name + ": " + command; + } } \ No newline at end of file diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java index 0b6e22e..52db0ce 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java @@ -48,6 +48,8 @@ import java.util.Set; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.DefaultParser; @@ -202,7 +204,7 @@ public class Runner extends Component { private static final String FW_VARS = "fw-vars.fd"; private static int exitStatus; - private EventPipeline rep; + private final EventPipeline rep = newEventPipeline(); private final ObjectMapper yamlMapper = new ObjectMapper(YAMLFactory .builder().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER) .build()); @@ -318,6 +320,7 @@ public class Runner extends Component { }); } + @SuppressWarnings("PMD.LambdaCanBeMethodReference") private void processInitialConfiguration(Configuration newConfig) { try { config = newConfig; @@ -333,12 +336,15 @@ public class Runner extends Component { var tplData = dataFromTemplate(); swtpmDefinition = Optional.ofNullable(tplData.get(SWTPM)) .map(d -> new CommandDefinition(SWTPM, d)).orElse(null); + logger.finest(() -> swtpmDefinition.toString()); qemuDefinition = Optional.ofNullable(tplData.get(QEMU)) .map(d -> new CommandDefinition(QEMU, d)).orElse(null); + logger.finest(() -> qemuDefinition.toString()); cloudInitImgDefinition = Optional.ofNullable(tplData.get(CLOUD_INIT_IMG)) .map(d -> new CommandDefinition(CLOUD_INIT_IMG, d)) .orElse(null); + logger.finest(() -> cloudInitImgDefinition.toString()); // Forward some values to child components qemuMonitor.configure(config.monitorSocket, @@ -364,6 +370,12 @@ public class Runner extends Component { break; } } + if (codePaths.iterator().hasNext() && config.firmwareRom == null) { + throw new IllegalArgumentException("No ROM found, candidates were: " + + StreamSupport.stream(codePaths.spliterator(), false) + .map(JsonNode::asText).collect(Collectors.joining(", "))); + } + // Get file for firmware vars, if necessary config.firmwareVars = config.dataDir.resolve(FW_VARS); if (!Files.exists(config.firmwareVars)) { @@ -405,12 +417,14 @@ public class Runner extends Component { model.put("hasDisplayPassword", config.hasDisplayPassword); model.put("cloudInit", config.cloudInit); model.put("vm", config.vm); + logger.finest(() -> "Processing template with model: " + model); // Combine template and data and parse result // (tempting, but no need to use a pipe here) var fmTemplate = fmConfig.getTemplate(templatePath.toString()); StringWriter out = new StringWriter(); fmTemplate.process(model, out); + logger.finest(() -> "Result of processing template: " + out); return yamlMapper.readValue(out.toString(), JsonNode.class); } @@ -432,8 +446,7 @@ public class Runner extends Component { // https://github.com/kubernetes-client/java/issues/100 io.kubernetes.client.openapi.Configuration.setDefaultApiClient(null); - // Prepare specific event pipeline to avoid concurrency. - rep = newEventPipeline(); + // Provide specific event pipeline to avoid concurrency. event.setAssociated(EventPipeline.class, rep); try { // Store process id @@ -746,6 +759,10 @@ public class Runner extends Component { props = Runner.class.getResourceAsStream("logging.properties"); } LogManager.getLogManager().readConfiguration(props); + Logger.getLogger(Runner.class.getName()).log(Level.CONFIG, + () -> path.isPresent() + ? "Using logging configuration from " + path.get() + : "Using default logging configuration"); } catch (IOException e) { e.printStackTrace(); }