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.
|
||||
# "namespace": ...
|
||||
|
||||
# Defines data for generating a cloud-init ISO image that is
|
||||
# attached to the VM.
|
||||
# "cloudInit":
|
||||
# "metaData":
|
||||
# ...
|
||||
# "userData":
|
||||
# ...
|
||||
|
||||
# Define the VM (required)
|
||||
"vm":
|
||||
# The VM's name (required)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import java.nio.charset.StandardCharsets;
|
|||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
|
@ -65,10 +66,23 @@ public class Configuration implements Dto {
|
|||
/** The firmware vars. */
|
||||
public Path firmwareVars;
|
||||
|
||||
/** Optional cloud-init data. */
|
||||
public CloudInit cloudInit;
|
||||
|
||||
/** The vm. */
|
||||
@SuppressWarnings("PMD.ShortVariable")
|
||||
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".
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ FROM docker.io/alpine
|
|||
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ RUN pacman-key --init \
|
|||
&& pacman -Sy --noconfirm archlinux-keyring && pacman -Su --noconfirm \
|
||||
&& pacman -S --noconfirm which qemu-full virtiofsd \
|
||||
edk2-ovmf swtpm iproute2 bridge-utils jre17-openjdk-headless \
|
||||
mtools \
|
||||
&& pacman -Scc --noconfirm
|
||||
|
||||
# Remove all targets.
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.JsonMappingException;
|
|||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
|
||||
import freemarker.core.ParseException;
|
||||
import freemarker.template.MalformedTemplateNameException;
|
||||
import freemarker.template.TemplateException;
|
||||
|
|
@ -40,6 +41,7 @@ import java.nio.file.Path;
|
|||
import java.nio.file.Paths;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
|
@ -178,9 +180,12 @@ import org.jgrapes.util.events.WatchFile;
|
|||
*
|
||||
*/
|
||||
@SuppressWarnings({ "PMD.ExcessiveImports", "PMD.AvoidPrintStackTrace",
|
||||
"PMD.DataflowAnomalyAnalysis" })
|
||||
"PMD.DataflowAnomalyAnalysis", "PMD.TooManyMethods" })
|
||||
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
|
||||
= "/opt/" + APP_NAME.replace("-", "") + "/templates";
|
||||
private static final String DEFAULT_TEMPLATE
|
||||
|
|
@ -190,16 +195,29 @@ public class Runner extends Component {
|
|||
private static int exitStatus;
|
||||
|
||||
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;
|
||||
@SuppressWarnings("PMD.UseConcurrentHashMap")
|
||||
private Configuration config = new Configuration();
|
||||
private final freemarker.template.Configuration fmConfig;
|
||||
private CommandDefinition swtpmDefinition;
|
||||
private CommandDefinition cloudInitImgDefinition;
|
||||
private CommandDefinition qemuDefinition;
|
||||
private final QemuMonitor qemuMonitor;
|
||||
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.
|
||||
*
|
||||
|
|
@ -293,10 +311,14 @@ public class Runner extends Component {
|
|||
|
||||
// Obtain more context data from template
|
||||
var tplData = dataFromTemplate();
|
||||
swtpmDefinition = Optional.ofNullable(tplData.get("swtpm"))
|
||||
.map(d -> new CommandDefinition("swtpm", d)).orElse(null);
|
||||
qemuDefinition = Optional.ofNullable(tplData.get("qemu"))
|
||||
.map(d -> new CommandDefinition("qemu", d)).orElse(null);
|
||||
swtpmDefinition = Optional.ofNullable(tplData.get(SWTPM))
|
||||
.map(d -> new CommandDefinition(SWTPM, d)).orElse(null);
|
||||
qemuDefinition = Optional.ofNullable(tplData.get(QEMU))
|
||||
.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
|
||||
qemuMonitor.configure(config.monitorSocket,
|
||||
|
|
@ -360,6 +382,7 @@ public class Runner extends Component {
|
|||
.map(Object::toString).orElse(null));
|
||||
model.put("firmwareVars", Optional.ofNullable(config.firmwareVars)
|
||||
.map(Object::toString).orElse(null));
|
||||
model.put("cloudInit", config.cloudInit);
|
||||
model.put("vm", config.vm);
|
||||
if (Optional.ofNullable(config.vm.display)
|
||||
.map(d -> d.spice).map(s -> s.ticket).isPresent()) {
|
||||
|
|
@ -430,12 +453,56 @@ public class Runner extends Component {
|
|||
state = State.STARTING;
|
||||
rep.fire(new RunnerStateChange(state, "RunnerStarted",
|
||||
"Runner has been started"));
|
||||
// Start first process
|
||||
// Start first process(es)
|
||||
qemuLatch.add(QemuPreps.Config);
|
||||
if (config.vm.useTpm && swtpmDefinition != null) {
|
||||
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) {
|
||||
|
|
@ -456,8 +523,8 @@ public class Runner extends Component {
|
|||
public void onFileChanged(FileChanged event) {
|
||||
if (event.change() == Kind.CREATED
|
||||
&& event.path().equals(config.swtpmSocket)) {
|
||||
// swtpm running, start qemu
|
||||
startProcess(qemuDefinition);
|
||||
// swtpm running, maybe start qemu
|
||||
mayBeStartQemu(QemuPreps.Tpm);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -545,7 +612,13 @@ public class Runner extends Component {
|
|||
@Handler
|
||||
public void onProcessExited(ProcessExited event, ProcessChannel channel) {
|
||||
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) {
|
||||
logger.severe(() -> "Process " + procDef.name
|
||||
+ " has exited with value " + event.exitValue()
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@
|
|||
- [ "--ctrl", "type=unixio,path=${ runtimeDir }/swtpm-sock,mode=0600" ]
|
||||
- "--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":
|
||||
# Candidate paths for the executable
|
||||
"executable": [ "/usr/bin/qemu-system-x86_64" ]
|
||||
|
|
@ -183,6 +196,16 @@
|
|||
<#break>
|
||||
</#switch>
|
||||
</#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.spice??>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue