From 3b3af742e3ecfe4a8e60350bef40d0d04a918703 Mon Sep 17 00:00:00 2001 From: Julian Labatut Date: Thu, 7 Apr 2022 21:58:10 +0200 Subject: [PATCH] quark: Implement `build` command - fetches `kaps` from GitHub (https://github.com/virt-do/kaps), builds it with `cargo`. - clones Cloud Hypervisor repository (https://github.com/cloud-hypervisor/linux), builds a Linux kernel using a specific configuration (https://github.com/virt-do/lumper/blob/main/kernel/linux-config-x86_64). - if offline mode is selected, fetches a `.tar.gz` archive from an URL an builds a container bundle containing the rootfs of the container and a `runc` config file. - creates an initrootfs image (`lzma` format) from an `alpine` image, containing the container bundle, the `kaps` binary and an init file to launch `kaps` using the bundle. - wraps the kernel, the initrootfs image and a `quark.json` config file inside a `quardle`. Signed-off-by: Julian Labatut --- .gitignore | 10 +- Cargo.toml | 7 +- README.md | 22 +++- scripts/mkbundle.sh | 24 +++++ scripts/mkkernel.sh | 16 +++ scripts/mkrootfs.sh | 34 ++++++ src/cli/build.rs | 257 ++++++++++++++++++++++++++++++++++++++++++-- src/cli/mod.rs | 2 + 8 files changed, 359 insertions(+), 13 deletions(-) create mode 100755 scripts/mkbundle.sh create mode 100755 scripts/mkkernel.sh create mode 100755 scripts/mkrootfs.sh diff --git a/.gitignore b/.gitignore index 53649aa..471d596 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,12 @@ Cargo.lock .idea *.tar.gz .vscode -ctr-bundle \ No newline at end of file + +# Generated on quark build +ctr-bundle +kaps +linux-cloud-hypervisor +alpine-minirootfs +initramfs.img +quark.json +*.qrk \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index eee4325..a034450 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,4 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -clap = { version = "3.0.5", features = ["derive"] } \ No newline at end of file +clap = { version = "3.0.5", features = ["derive"] } +git2 = "0.14.2" +serde = { version = "1.0.136", features = ["derive"] } +serde_json = "1.0.79" +tar = "0.4.38" +flate2 = "1.0.23" \ No newline at end of file diff --git a/README.md b/README.md index 8f9a918..59546fa 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,23 @@ -# Lumper +# Quark + +## Build a quardle + +Building a `quardle` with the container image bundled into the initramfs image : + +```bash +quark build --image --offline --quardle +``` + +Building a `quardle` with the container image to be pulled from within the guest: + +```bash +quark build --image --quardle +``` + +## Run a quardle + +```bash +quark run --quardle --output +``` diff --git a/scripts/mkbundle.sh b/scripts/mkbundle.sh new file mode 100755 index 0000000..6cb60e9 --- /dev/null +++ b/scripts/mkbundle.sh @@ -0,0 +1,24 @@ +# From https://github.com/virt-do/kaps/blob/main/hack/mkbundle.sh + +# The URL of a container image archive should be supplied in parameter +# Example with alpine : https://dl-cdn.alpinelinux.org/alpine/v3.14/releases/x86_64/alpine-minirootfs-3.14.2-x86_64.tar.gz +if [[ $# -ne 1 ]]; then + echo "Missing container image URL" + exit 1 +fi + +DEST="ctr-bundle" +IMAGE_ARCHIVE_URL=$1 +IMAGE_ARCHIVE_NAME="imageArchive" + +rm -rf $DEST $IMAGE_ARCHIVE_NAME && mkdir -p "$DEST"/rootfs + +# Download and untar the image +curl -sSL $IMAGE_ARCHIVE_URL -o $IMAGE_ARCHIVE_NAME +tar xf $IMAGE_ARCHIVE_NAME -C "$DEST"/rootfs +rm $IMAGE_ARCHIVE_NAME + +pushd "$DEST" > /dev/null +# Generate a runtime spec +runc spec --rootless +popd > /dev/null diff --git a/scripts/mkkernel.sh b/scripts/mkkernel.sh new file mode 100755 index 0000000..ad98b1c --- /dev/null +++ b/scripts/mkkernel.sh @@ -0,0 +1,16 @@ +# From https://github.com/virt-do/lumper/blob/main/kernel/mkkernel.sh + +CONFIG_URL="https://raw.githubusercontent.com/virt-do/lumper/main/kernel/linux-config-x86_64" +LINUX_REPO=linux-cloud-hypervisor + +if [ ! -d $LINUX_REPO ] +then + echo "Cloning linux-cloud-hypervisor git repository..." + git clone --depth 1 "https://github.com/cloud-hypervisor/linux.git" -b "ch-5.14" $LINUX_REPO +fi + +pushd $LINUX_REPO > /dev/null +pwd +wget -qO .config $CONFIG_URL +make bzImage -j `nproc` +popd > /dev/null diff --git a/scripts/mkrootfs.sh b/scripts/mkrootfs.sh new file mode 100755 index 0000000..2401e07 --- /dev/null +++ b/scripts/mkrootfs.sh @@ -0,0 +1,34 @@ +# From https://github.com/virt-do/lumper/blob/main/rootfs/mkrootfs.sh + +DEST="alpine-minirootfs" +IMAGE_ARCHIVE_URL="https://dl-cdn.alpinelinux.org/alpine/v3.14/releases/x86_64/alpine-minirootfs-3.14.2-x86_64.tar.gz" +IMAGE_ARCHIVE_NAME="imageArchive" + +rm -rf $DEST $IMAGE_ARCHIVE_NAME && mkdir $DEST + +# Download and untar the image +curl -sSL $IMAGE_ARCHIVE_URL -o $IMAGE_ARCHIVE_NAME +tar xf $IMAGE_ARCHIVE_NAME -C $DEST +rm $IMAGE_ARCHIVE_NAME + +pushd "$DEST" > /dev/null +# Create an init file in the rootfs +cat > init < /dev/null + +# Here we do not create the rootfs image because we need to add some files in it later \ No newline at end of file diff --git a/src/cli/build.rs b/src/cli/build.rs index 84f6e7a..571868a 100644 --- a/src/cli/build.rs +++ b/src/cli/build.rs @@ -1,29 +1,266 @@ use clap::Args; -use super::{Handler, Result}; +use super::{Error as QuarkError, Handler}; -/// Arguments for `BuildCommand` -/// -/// Usage : -/// `quark build --image ` +use flate2::{write::GzEncoder, Compression}; +use git2::Repository; +use serde::{Deserialize, Serialize}; +use std::fs::{copy, remove_dir_all, remove_file, File}; +use std::io::{BufRead, BufReader, Error, ErrorKind}; +use std::path::Path; +use std::process::{Command, Stdio}; +use tar::Builder; + +const BUNDLE_DIR: &str = "ctr-bundle/"; +const CONFIG_FILE: &str = "quark.json"; +const DEFAULT_CONTAINER_IMAGE_URL: &str = "https://dl-cdn.alpinelinux.org/alpine/v3.14/releases/x86_64/alpine-minirootfs-3.14.2-x86_64.tar.gz"; +const INITRAMFS_NAME: &str = "initramfs.img"; +const KAPS_PATH: &str = "kaps/target/x86_64-unknown-linux-musl/release/kaps"; +const KERNEL_CMDLINE: &str = "console=ttyS0 i8042.nokbd reboot=k panic=1 pci=off"; +const KERNEL_PATH: &str = "linux-cloud-hypervisor/arch/x86/boot/compressed/vmlinux.bin"; +const ROOTFS_DIR: &str = "alpine-minirootfs/"; + +/// Builds a quardle, containing a VM rootfs, a linux kernel, +/// kaps binary and an optional container image bundle #[derive(Debug, Args)] +#[clap(about)] pub struct BuildCommand { - /// The name of the generated quardle, with the suffix `.qrk` + /// The name of the generated quardle (the suffix `.qrk` will be added) #[clap(short, long)] quardle: String, - /// Indicates wether or not the container image is bundled into the initramfs image + /// The container image url to use. It should be an URL of a `.tar.gz` archive + #[clap(short, long, default_value = DEFAULT_CONTAINER_IMAGE_URL)] + image: String, + + /// Indicates if the container image should be bundled into the VM rootfs #[clap(short, long)] offline: bool, /// Overrides the default kernel command line - #[clap(short, long)] - kernel_cmd: Option, + #[clap(short, long, default_value = KERNEL_CMDLINE)] + kernel_cmdline: String, +} + +/// Describes the content of the `quark.json` config file +#[derive(Serialize, Deserialize, Debug)] +struct QuarkFile { + /// The name of the generated quardle + quardle: String, + /// The kernel file name + kernel: String, + /// The initramfs image file name + initramfs: String, + /// The kernel command line + kernel_cmdline: String, + /// The container image url to use + image: String, + /// The kaps binary path + kaps: String, + /// States if the initramfs contains the container image + offline: bool, + /// If offline, states the name of the container image directory + bundle: Option, } /// Method that will be called when the command is executed. impl Handler for BuildCommand { - fn handler(&self) -> Result<()> { + fn handler(&self) -> Result<(), QuarkError> { + // Fetch & Build all prerequisites + PreBuild::build_kaps("https://github.com/virt-do/kaps.git")?; + PreBuild::build_kernel()?; + if self.offline { + PreBuild::build_bundle(&self.image)?; + } + PreBuild::build_rootfs(self.offline)?; + + // Create the JSON config file + let config_file = QuarkFile { + quardle: self.quardle.clone(), + kernel: "vmlinux.bin".to_string(), + initramfs: INITRAMFS_NAME.to_string(), + image: self.image.clone(), + kernel_cmdline: self.kernel_cmdline.clone(), + kaps: "/opt/kaps".to_string(), + offline: self.offline, + bundle: if self.offline { + Some(format!("/{}", BUNDLE_DIR)) + } else { + None + }, + }; + + serde_json::to_writer_pretty(File::create(CONFIG_FILE)?, &config_file) + .map_err(QuarkError::Serialize)?; + // Create the archive + println!("Creating the archive..."); + let quardle_name: &str = &format!("{}.qrk", self.quardle); + let mut archive = Builder::new(GzEncoder::new( + File::create(quardle_name)?, + Compression::default(), + )); + archive.append_file(CONFIG_FILE, &mut File::open(CONFIG_FILE)?)?; + archive.append_file("vmlinux.bin", &mut File::open(KERNEL_PATH)?)?; + archive.append_file(INITRAMFS_NAME, &mut File::open(INITRAMFS_NAME)?)?; + + archive.finish()?; + + println!("{} has been created.", quardle_name); + + // Clean temporary files and directories (keeping kaps and kernel beceause of their size) + remove_dir_all(ROOTFS_DIR)?; + remove_dir_all(BUNDLE_DIR)?; + remove_file(INITRAMFS_NAME)?; + remove_file(CONFIG_FILE)?; + Ok(()) + } +} + +pub struct PreBuild {} +impl PreBuild { + /// When kaps will have its own release on GitHub, the binary could be fetched here. + /// Meanwhile, we are using git2 library to clone Kaps from GitHub, then cargo to build it from the sources. + /// If the kaps binary exists, skipping this step. + pub fn build_kaps(kaps_repository_url: &str) -> Result<(), QuarkError> { + if Path::new(KAPS_PATH).exists() { + println!("Kaps binary already exists, skipping."); + return Ok(()); + } + println!("Cloning the Kaps GitHub repository..."); + Repository::clone(kaps_repository_url, "kaps").map_err(QuarkError::Git)?; + // Since there is a bug in the latests versions of kaps to build it with a musl target, + // we need to checkout to a working version. + Command::new("sh") + .arg("-c") + .arg("cd kaps && git checkout cdce0eb") + .output()?; + + println!("Building kaps binary..."); + let stdout = Command::new("sh") + .arg("-c") + .arg("cd kaps && cargo build --release --target=x86_64-unknown-linux-musl") + .stdout(Stdio::piped()) + .spawn()? + .stdout + .ok_or_else(|| Error::new(ErrorKind::Other, "Failed to execute cargo build"))?; + let reader = BufReader::new(stdout); + reader + .lines() + .filter_map(|line| line.ok()) + .for_each(|line| println!("{}", line)); + Ok(()) + } + + /// Building kernel from cloud-hypervisor/linux Git repository, with a *x86_64* config. + /// If the `kernel` exists, skipping this step. + pub fn build_kernel() -> Result<(), QuarkError> { + if Path::new(KERNEL_PATH).exists() { + println!("Kernel already exists, no need to re-build it, skipping."); + return Ok(()); + } + println!("Building kernel..."); + let stdout = Command::new("bash") + .arg("-c") + .arg("kernel/mkkernel.sh") + .stdout(Stdio::piped()) + .spawn()? + .stdout + .ok_or_else(|| Error::new(ErrorKind::Other, "Failed to build kernel"))?; + let reader = BufReader::new(stdout); + reader + .lines() + .filter_map(|line| line.ok()) + .for_each(|line| println!("{}", line)); + Ok(()) + } + + /// Creates the container bundle, containing the image inside a rootfs folder + /// and a runc spec (config.json). + pub fn build_bundle(image_url: &str) -> Result<(), QuarkError> { + if Path::new(BUNDLE_DIR).exists() { + println!("Container bundle already exists, skipping."); + return Ok(()); + } + println!("Creating container bundle..."); + let stdout = Command::new("bash") + .arg("-c") + .arg(format!("scripts/mkbundle.sh {}", image_url)) + .stdout(Stdio::piped()) + .spawn()? + .stdout + .ok_or_else(|| Error::new(ErrorKind::Other, "Failed to build bundle"))?; + let reader = BufReader::new(stdout); + reader + .lines() + .filter_map(|line| line.ok()) + .for_each(|line| println!("{}", line)); + + Ok(()) + } + + /// Creates the alpine-minirootfs folder, fetched from + /// https://dl-cdn.alpinelinux.org/alpine/v3.14/releases/x86_64/alpine-minirootfs-3.14.2-x86_64.tar.gz, + /// with an init file, and copy kaps binary (and container bundle if offline mode). + /// If the folder exists, skipping this step. + pub fn build_rootfs(offline: bool) -> Result<(), QuarkError> { + if Path::new(INITRAMFS_NAME).exists() { + println!("rootfs image already exists, skipping."); + return Ok(()); + } + BufReader::new( + Command::new("bash") + .arg("-c") + .arg("scripts/mkrootfs.sh") + .stdout(Stdio::piped()) + .spawn()? + .stdout + .ok_or_else(|| Error::new(ErrorKind::Other, "Failed to build rootfs"))?, + ) + .lines() + .filter_map(|line| line.ok()) + .for_each(|line| println!("{}", line)); + // Adding Kaps binary to the rootfs + copy(KAPS_PATH, format!("{}opt/kaps", ROOTFS_DIR))?; + + if offline { + // Adding the container bundle to the rootfs + // (Cannot use copy function from std::fs because there is a lot of files to move) + BufReader::new( + Command::new("cp") + .arg("-r") + .arg(BUNDLE_DIR) + .arg(ROOTFS_DIR) + .stdout(Stdio::piped()) + .spawn()? + .stdout + .ok_or_else(|| { + Error::new(ErrorKind::Other, "Failed to copy bundle into rootfs") + })?, + ) + .lines() + .filter_map(|line| line.ok()) + .for_each(|line| println!("{}", line)); + } + + println!("Creating initramfs image..."); + BufReader::new( + Command::new("bash") + .arg("-c") + .arg(format!( + "cd {} && find . -print0 | + cpio --null --create --owner root:root --format=newc | + xz -9 --format=lzma > ../{}", + ROOTFS_DIR, INITRAMFS_NAME + )) + .stdout(Stdio::piped()) + .spawn()? + .stdout + .ok_or_else(|| Error::new(ErrorKind::Other, "Failed to build initramfs image"))?, + ) + .lines() + .filter_map(|line| line.ok()) + .for_each(|line| println!("{}", line)); + Ok(()) } } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 4ada7fb..5997d2f 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -8,6 +8,8 @@ use clap::{Parser, Subcommand}; #[derive(Debug)] pub enum Error { OpenFile(std::io::Error), + Serialize(serde_json::Error), + Git(git2::Error), } impl From for Error {