Add cloud-init support in runner.
This commit is contained in:
parent
b5622a459c
commit
24f762d28c
6 changed files with 132 additions and 13 deletions
|
|
@ -27,6 +27,14 @@
|
||||||
# be set when starting the runner during development e.g. from the IDE.
|
# be set when starting the runner during development e.g. from the IDE.
|
||||||
# "namespace": ...
|
# "namespace": ...
|
||||||
|
|
||||||
|
# Defines data for generating a cloud-init ISO image that is
|
||||||
|
# attached to the VM.
|
||||||
|
# "cloudInit":
|
||||||
|
# "metaData":
|
||||||
|
# ...
|
||||||
|
# "userData":
|
||||||
|
# ...
|
||||||
|
|
||||||
# Define the VM (required)
|
# Define the VM (required)
|
||||||
"vm":
|
"vm":
|
||||||
# The VM's name (required)
|
# The VM's name (required)
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.attribute.PosixFilePermission;
|
import java.nio.file.attribute.PosixFilePermission;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
@ -65,10 +66,23 @@ public class Configuration implements Dto {
|
||||||
/** The firmware vars. */
|
/** The firmware vars. */
|
||||||
public Path firmwareVars;
|
public Path firmwareVars;
|
||||||
|
|
||||||
|
/** Optional cloud-init data. */
|
||||||
|
public CloudInit cloudInit;
|
||||||
|
|
||||||
/** The vm. */
|
/** The vm. */
|
||||||
@SuppressWarnings("PMD.ShortVariable")
|
@SuppressWarnings("PMD.ShortVariable")
|
||||||
public Vm vm;
|
public Vm vm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subsection "cloud-init".
|
||||||
|
*/
|
||||||
|
public static class CloudInit implements Dto {
|
||||||
|
@SuppressWarnings("PMD.UseConcurrentHashMap")
|
||||||
|
public Map<String, Object> metaData;
|
||||||
|
@SuppressWarnings("PMD.UseConcurrentHashMap")
|
||||||
|
public Map<String, Object> userData;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subsection "vm".
|
* Subsection "vm".
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ FROM docker.io/alpine
|
||||||
|
|
||||||
RUN apk update
|
RUN apk update
|
||||||
|
|
||||||
RUN apk add qemu-system-x86_64 qemu-modules ovmf swtpm openjdk17
|
RUN apk add qemu-system-x86_64 qemu-modules ovmf swtpm openjdk17 mtools
|
||||||
|
|
||||||
RUN mkdir -p /etc/qemu && echo "allow all" > /etc/qemu/bridge.conf
|
RUN mkdir -p /etc/qemu && echo "allow all" > /etc/qemu/bridge.conf
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ RUN pacman-key --init \
|
||||||
&& pacman -Sy --noconfirm archlinux-keyring && pacman -Su --noconfirm \
|
&& pacman -Sy --noconfirm archlinux-keyring && pacman -Su --noconfirm \
|
||||||
&& pacman -S --noconfirm which qemu-full virtiofsd \
|
&& pacman -S --noconfirm which qemu-full virtiofsd \
|
||||||
edk2-ovmf swtpm iproute2 bridge-utils jre17-openjdk-headless \
|
edk2-ovmf swtpm iproute2 bridge-utils jre17-openjdk-headless \
|
||||||
|
mtools \
|
||||||
&& pacman -Scc --noconfirm
|
&& pacman -Scc --noconfirm
|
||||||
|
|
||||||
# Remove all targets.
|
# Remove all targets.
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.JsonMappingException;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||||
|
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
|
||||||
import freemarker.core.ParseException;
|
import freemarker.core.ParseException;
|
||||||
import freemarker.template.MalformedTemplateNameException;
|
import freemarker.template.MalformedTemplateNameException;
|
||||||
import freemarker.template.TemplateException;
|
import freemarker.template.TemplateException;
|
||||||
|
|
@ -40,6 +41,7 @@ import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
@ -178,9 +180,12 @@ import org.jgrapes.util.events.WatchFile;
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({ "PMD.ExcessiveImports", "PMD.AvoidPrintStackTrace",
|
@SuppressWarnings({ "PMD.ExcessiveImports", "PMD.AvoidPrintStackTrace",
|
||||||
"PMD.DataflowAnomalyAnalysis" })
|
"PMD.DataflowAnomalyAnalysis", "PMD.TooManyMethods" })
|
||||||
public class Runner extends Component {
|
public class Runner extends Component {
|
||||||
|
|
||||||
|
private static final String QEMU = "qemu";
|
||||||
|
private static final String SWTPM = "swtpm";
|
||||||
|
private static final String CLOUD_INIT_IMG = "cloudInitImg";
|
||||||
private static final String TEMPLATE_DIR
|
private static final String TEMPLATE_DIR
|
||||||
= "/opt/" + APP_NAME.replace("-", "") + "/templates";
|
= "/opt/" + APP_NAME.replace("-", "") + "/templates";
|
||||||
private static final String DEFAULT_TEMPLATE
|
private static final String DEFAULT_TEMPLATE
|
||||||
|
|
@ -190,16 +195,29 @@ public class Runner extends Component {
|
||||||
private static int exitStatus;
|
private static int exitStatus;
|
||||||
|
|
||||||
private EventPipeline rep;
|
private EventPipeline rep;
|
||||||
private final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
|
private final ObjectMapper yamlMapper = new ObjectMapper(YAMLFactory
|
||||||
|
.builder().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)
|
||||||
|
.build());
|
||||||
private final JsonNode defaults;
|
private final JsonNode defaults;
|
||||||
@SuppressWarnings("PMD.UseConcurrentHashMap")
|
@SuppressWarnings("PMD.UseConcurrentHashMap")
|
||||||
private Configuration config = new Configuration();
|
private Configuration config = new Configuration();
|
||||||
private final freemarker.template.Configuration fmConfig;
|
private final freemarker.template.Configuration fmConfig;
|
||||||
private CommandDefinition swtpmDefinition;
|
private CommandDefinition swtpmDefinition;
|
||||||
|
private CommandDefinition cloudInitImgDefinition;
|
||||||
private CommandDefinition qemuDefinition;
|
private CommandDefinition qemuDefinition;
|
||||||
private final QemuMonitor qemuMonitor;
|
private final QemuMonitor qemuMonitor;
|
||||||
private State state = State.INITIALIZING;
|
private State state = State.INITIALIZING;
|
||||||
|
|
||||||
|
/** Preparatory actions for QEMU start */
|
||||||
|
@SuppressWarnings("PMD.FieldNamingConventions")
|
||||||
|
private enum QemuPreps {
|
||||||
|
Config,
|
||||||
|
Tpm,
|
||||||
|
CloudInit
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Set<QemuPreps> qemuLatch = new HashSet<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a new runner.
|
* Instantiates a new runner.
|
||||||
*
|
*
|
||||||
|
|
@ -293,10 +311,14 @@ public class Runner extends Component {
|
||||||
|
|
||||||
// Obtain more context data from template
|
// Obtain more context data from template
|
||||||
var tplData = dataFromTemplate();
|
var tplData = dataFromTemplate();
|
||||||
swtpmDefinition = Optional.ofNullable(tplData.get("swtpm"))
|
swtpmDefinition = Optional.ofNullable(tplData.get(SWTPM))
|
||||||
.map(d -> new CommandDefinition("swtpm", d)).orElse(null);
|
.map(d -> new CommandDefinition(SWTPM, d)).orElse(null);
|
||||||
qemuDefinition = Optional.ofNullable(tplData.get("qemu"))
|
qemuDefinition = Optional.ofNullable(tplData.get(QEMU))
|
||||||
.map(d -> new CommandDefinition("qemu", d)).orElse(null);
|
.map(d -> new CommandDefinition(QEMU, d)).orElse(null);
|
||||||
|
cloudInitImgDefinition
|
||||||
|
= Optional.ofNullable(tplData.get(CLOUD_INIT_IMG))
|
||||||
|
.map(d -> new CommandDefinition(CLOUD_INIT_IMG, d))
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
// Forward some values to child components
|
// Forward some values to child components
|
||||||
qemuMonitor.configure(config.monitorSocket,
|
qemuMonitor.configure(config.monitorSocket,
|
||||||
|
|
@ -360,6 +382,7 @@ public class Runner extends Component {
|
||||||
.map(Object::toString).orElse(null));
|
.map(Object::toString).orElse(null));
|
||||||
model.put("firmwareVars", Optional.ofNullable(config.firmwareVars)
|
model.put("firmwareVars", Optional.ofNullable(config.firmwareVars)
|
||||||
.map(Object::toString).orElse(null));
|
.map(Object::toString).orElse(null));
|
||||||
|
model.put("cloudInit", config.cloudInit);
|
||||||
model.put("vm", config.vm);
|
model.put("vm", config.vm);
|
||||||
if (Optional.ofNullable(config.vm.display)
|
if (Optional.ofNullable(config.vm.display)
|
||||||
.map(d -> d.spice).map(s -> s.ticket).isPresent()) {
|
.map(d -> d.spice).map(s -> s.ticket).isPresent()) {
|
||||||
|
|
@ -430,12 +453,56 @@ public class Runner extends Component {
|
||||||
state = State.STARTING;
|
state = State.STARTING;
|
||||||
rep.fire(new RunnerStateChange(state, "RunnerStarted",
|
rep.fire(new RunnerStateChange(state, "RunnerStarted",
|
||||||
"Runner has been started"));
|
"Runner has been started"));
|
||||||
// Start first process
|
// Start first process(es)
|
||||||
|
qemuLatch.add(QemuPreps.Config);
|
||||||
if (config.vm.useTpm && swtpmDefinition != null) {
|
if (config.vm.useTpm && swtpmDefinition != null) {
|
||||||
startProcess(swtpmDefinition);
|
startProcess(swtpmDefinition);
|
||||||
return;
|
qemuLatch.add(QemuPreps.Tpm);
|
||||||
|
}
|
||||||
|
if (config.cloudInit != null) {
|
||||||
|
generateCloudInitImg();
|
||||||
|
qemuLatch.add(QemuPreps.CloudInit);
|
||||||
|
}
|
||||||
|
mayBeStartQemu(QemuPreps.Config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mayBeStartQemu(QemuPreps done) {
|
||||||
|
synchronized (qemuLatch) {
|
||||||
|
if (qemuLatch.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
qemuLatch.remove(done);
|
||||||
|
if (qemuLatch.isEmpty()) {
|
||||||
|
startProcess(qemuDefinition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateCloudInitImg() {
|
||||||
|
try {
|
||||||
|
var cloudInitDir = config.dataDir.resolve("cloud-init");
|
||||||
|
cloudInitDir.toFile().mkdir();
|
||||||
|
var metaOut
|
||||||
|
= Files.newBufferedWriter(cloudInitDir.resolve("meta-data"));
|
||||||
|
if (config.cloudInit.metaData != null) {
|
||||||
|
yamlMapper.writer().writeValue(metaOut,
|
||||||
|
config.cloudInit.metaData);
|
||||||
|
}
|
||||||
|
metaOut.close();
|
||||||
|
var userOut
|
||||||
|
= Files.newBufferedWriter(cloudInitDir.resolve("user-data"));
|
||||||
|
userOut.write("#cloud-config\n");
|
||||||
|
if (config.cloudInit.userData != null) {
|
||||||
|
yamlMapper.writer().writeValue(userOut,
|
||||||
|
config.cloudInit.userData);
|
||||||
|
}
|
||||||
|
userOut.close();
|
||||||
|
startProcess(cloudInitImgDefinition);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.log(Level.SEVERE, e,
|
||||||
|
() -> "Cannot start runner: " + e.getMessage());
|
||||||
|
fire(new Stop());
|
||||||
}
|
}
|
||||||
startProcess(qemuDefinition);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean startProcess(CommandDefinition toStart) {
|
private boolean startProcess(CommandDefinition toStart) {
|
||||||
|
|
@ -456,8 +523,8 @@ public class Runner extends Component {
|
||||||
public void onFileChanged(FileChanged event) {
|
public void onFileChanged(FileChanged event) {
|
||||||
if (event.change() == Kind.CREATED
|
if (event.change() == Kind.CREATED
|
||||||
&& event.path().equals(config.swtpmSocket)) {
|
&& event.path().equals(config.swtpmSocket)) {
|
||||||
// swtpm running, start qemu
|
// swtpm running, maybe start qemu
|
||||||
startProcess(qemuDefinition);
|
mayBeStartQemu(QemuPreps.Tpm);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -545,7 +612,13 @@ public class Runner extends Component {
|
||||||
@Handler
|
@Handler
|
||||||
public void onProcessExited(ProcessExited event, ProcessChannel channel) {
|
public void onProcessExited(ProcessExited event, ProcessChannel channel) {
|
||||||
channel.associated(CommandDefinition.class).ifPresent(procDef -> {
|
channel.associated(CommandDefinition.class).ifPresent(procDef -> {
|
||||||
// No process(es) may exit during startup
|
if (procDef.equals(cloudInitImgDefinition)
|
||||||
|
&& event.exitValue() == 0) {
|
||||||
|
// Cloud-init ISO generation was successful.
|
||||||
|
mayBeStartQemu(QemuPreps.CloudInit);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// No other process(es) may exit during startup
|
||||||
if (state == State.STARTING) {
|
if (state == State.STARTING) {
|
||||||
logger.severe(() -> "Process " + procDef.name
|
logger.severe(() -> "Process " + procDef.name
|
||||||
+ " has exited with value " + event.exitValue()
|
+ " has exited with value " + event.exitValue()
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,19 @@
|
||||||
- [ "--ctrl", "type=unixio,path=${ runtimeDir }/swtpm-sock,mode=0600" ]
|
- [ "--ctrl", "type=unixio,path=${ runtimeDir }/swtpm-sock,mode=0600" ]
|
||||||
- "--terminate"
|
- "--terminate"
|
||||||
|
|
||||||
|
"cloudInitImg":
|
||||||
|
# Candidate paths for the executable
|
||||||
|
"executable": [ "/bin/sh", "/usr/bin/sh" ]
|
||||||
|
|
||||||
|
# Arguments may be specified as nested lists for better readability.
|
||||||
|
# The arguments are flattened before being passed to the process.
|
||||||
|
"arguments":
|
||||||
|
- "-c"
|
||||||
|
- >-
|
||||||
|
mformat -C -f 1440 -v CIDATA -i ${ runtimeDir }/cloud-init.img
|
||||||
|
&& mcopy -i ${ runtimeDir }/cloud-init.img
|
||||||
|
${ dataDir }/cloud-init/meta-data ${ dataDir }/cloud-init/user-data ::
|
||||||
|
|
||||||
"qemu":
|
"qemu":
|
||||||
# Candidate paths for the executable
|
# Candidate paths for the executable
|
||||||
"executable": [ "/usr/bin/qemu-system-x86_64" ]
|
"executable": [ "/usr/bin/qemu-system-x86_64" ]
|
||||||
|
|
@ -183,6 +196,16 @@
|
||||||
<#break>
|
<#break>
|
||||||
</#switch>
|
</#switch>
|
||||||
</#list>
|
</#list>
|
||||||
|
# Cloud-init image
|
||||||
|
<#if cloudInit??>
|
||||||
|
- [ "-blockdev", "node-name=drive-${ drvCounter }-host-resource,\
|
||||||
|
driver=file,filename=${ runtimeDir }/cloud-init.img" ]
|
||||||
|
# - how to use the file (as sequence of literal blocks)
|
||||||
|
- [ "-blockdev", "node-name=drive-${ drvCounter }-backend,driver=raw,\
|
||||||
|
file=drive-${ drvCounter }-host-resource" ]
|
||||||
|
# - the driver (what the guest sees)
|
||||||
|
- [ "-device", "virtio-blk-pci,drive=drive-${ drvCounter }-backend" ]
|
||||||
|
</#if>
|
||||||
|
|
||||||
<#if vm.display??>
|
<#if vm.display??>
|
||||||
<#if vm.display.spice??>
|
<#if vm.display.spice??>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue