Skip to content

Commit

Permalink
Add cloud-init support in runner.
Browse files Browse the repository at this point in the history
  • Loading branch information
mnlipp committed Feb 17, 2024
1 parent b5622a4 commit 24f762d
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 13 deletions.
8 changes: 8 additions & 0 deletions org.jdrupes.vmoperator.runner.qemu/config-sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
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;
Expand Down Expand Up @@ -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".
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
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;
Expand All @@ -40,6 +41,7 @@
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;
Expand Down Expand Up @@ -178,9 +180,12 @@
*
*/
@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
Expand All @@ -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.
*
Expand Down Expand Up @@ -293,10 +311,14 @@ private void processInitialConfiguration(

// 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,
Expand Down Expand Up @@ -360,6 +382,7 @@ private JsonNode dataFromTemplate()
.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()) {
Expand Down Expand Up @@ -430,12 +453,56 @@ public void onStarted(Started event) {
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) {
Expand All @@ -456,8 +523,8 @@ private boolean startProcess(CommandDefinition toStart) {
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;
}
}
Expand Down Expand Up @@ -545,7 +612,13 @@ public void onConfigureQemu(RunnerConfigurationUpdate event) {
@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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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" ]
Expand Down Expand Up @@ -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??>
Expand Down

0 comments on commit 24f762d

Please sign in to comment.