diff --git a/rootfs/mkrootfs.sh b/rootfs/mkrootfs.sh deleted file mode 100755 index 8fcdc2e..0000000 --- a/rootfs/mkrootfs.sh +++ /dev/null @@ -1,25 +0,0 @@ -# From https://github.com/virt-do/lumper/blob/main/rootfs/mkrootfs.sh - -curl -O https://dl-cdn.alpinelinux.org/alpine/v3.14/releases/x86_64/alpine-minirootfs-3.14.2-x86_64.tar.gz - -mkdir alpine-minirootfs -tar xf alpine-minirootfs-3.14.2-x86_64.tar.gz -C alpine-minirootfs -rm alpine-minirootfs-3.14.2-x86_64.tar.gz - -pushd alpine-minirootfs -cat > init < /dev/null +# Generate a runtime spec +runc spec --rootless +popd > /dev/null diff --git a/kernel/mkkernel.sh b/scripts/mkkernel.sh similarity index 90% rename from kernel/mkkernel.sh rename to scripts/mkkernel.sh index 1d46898..ad98b1c 100755 --- a/kernel/mkkernel.sh +++ b/scripts/mkkernel.sh @@ -9,8 +9,8 @@ then git clone --depth 1 "https://github.com/cloud-hypervisor/linux.git" -b "ch-5.14" $LINUX_REPO fi -pushd $LINUX_REPO +pushd $LINUX_REPO > /dev/null pwd wget -qO .config $CONFIG_URL make bzImage -j `nproc` -popd +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 f98c3fd..9ff5b29 100644 --- a/src/cli/build.rs +++ b/src/cli/build.rs @@ -5,36 +5,41 @@ use super::{Handler, Result}; use flate2::{write::GzEncoder, Compression}; use git2::Repository; use serde::{Deserialize, Serialize}; -use std::fs::{write, File}; +use std::fs::{copy, remove_dir_all, remove_file, File}; use std::io::{BufRead, BufReader}; use std::path::Path; use std::process::{Command, Stdio}; use tar::Builder; -const KERNEL_CMD: &str = "console=ttyS0 i8042.nokbd reboot=k panic=1 pci=off"; +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/"; -/// Arguments for `BuildCommand` -/// -/// Usage : -/// `quark build --image [--offline] -/// [--kernel-cmd ] --quardle ` +/// 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, - /// The container image url to use - #[clap(short, long)] + /// 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 wether or not the container image is bundled into the initramfs image + /// 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 @@ -44,14 +49,18 @@ struct QuarkFile { quardle: String, /// The kernel file name kernel: String, - /// The initrd file name - initrd: String, + /// The initramfs image file name + initramfs: String, /// The kernel command line - kernel_cmd: String, + 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. @@ -60,62 +69,67 @@ impl Handler for BuildCommand { // Fetch & Build all prerequisites PreBuild::build_kaps("https://github.com/virt-do/kaps.git"); PreBuild::build_kernel(); - PreBuild::build_fs(); + if self.offline { + PreBuild::build_bundle(&self.image); + } + PreBuild::build_rootfs(self.offline); // Create the JSON config file - println!("Creating the config file..."); let config_file = QuarkFile { quardle: self.quardle.clone(), - kernel: "bzImage".to_string(), - initrd: "alpine-minirootfs".to_string(), + kernel: "vmlinux.bin".to_string(), + initramfs: INITRAMFS_NAME.to_string(), image: self.image.clone(), - kernel_cmd: match self.kernel_cmd.clone() { - Some(c) => c, - _ => KERNEL_CMD.to_string(), + kernel_cmdline: self.kernel_cmdline.clone(), + kaps: "/opt/kaps".to_string(), + offline: self.offline, + bundle: if self.offline { + Some(format!("/{}", BUNDLE_DIR.to_string())) + } else { + None }, - kaps: "alpine-minirootfs/opt/kaps".to_string(), }; - write( - "./quark.json", - serde_json::to_string_pretty(&config_file).unwrap(), + serde_json::to_writer_pretty( + File::create(CONFIG_FILE).expect("Unable to create the json config file"), + &config_file, ) - .expect("Unable to write to ./quark.json"); - + .expect("Unable to write to the json config file"); // Create the archive println!("Creating the archive..."); - let file = File::create(format!("{}.qrk", self.quardle)).unwrap(); - let mut archive = Builder::new(GzEncoder::new(file, Compression::default())); + let quardle_name: &str = &format!("{}.qrk", self.quardle); + let mut archive = Builder::new(GzEncoder::new( + File::create(quardle_name).unwrap(), + Compression::default(), + )); archive - .append_file("quark.json", &mut File::open("quark.json").unwrap()) - .unwrap(); + .append_file( + CONFIG_FILE, + &mut File::open(CONFIG_FILE).expect("Unable to open the json config file"), + ) + .expect("Unable to add the json config file to archive"); archive .append_file( - "bzImage", - &mut File::open("linux-cloud-hypervisor/arch/x86/boot/bzImage").unwrap(), + "vmlinux.bin", + &mut File::open(KERNEL_PATH).expect("Unable to open kernel"), ) - .unwrap(); - + .expect("Unable to add kernel to archive"); archive .append_file( - "alpine-minirootfs/opt/kaps", - &mut File::open("kaps/target/release/kaps").unwrap(), + INITRAMFS_NAME, + &mut File::open(INITRAMFS_NAME).expect("Unable to open initramfs image"), ) - .unwrap(); + .expect("Unable to add initramgs file to archive"); archive.finish().unwrap(); - // This cannot be used because the alpine-minirootfs folder contains sockets and some files with no write permissions, - // which cannot be archived using the `tar` package. - // archive.append_dir_all("alpine-minirootfs", "alpine-minirootfs").unwrap(); - // Thus, we use here the tar command to add the alpine-minirootfs to the existing archive. - Command::new("tar") - .arg("-rf") - .arg(format!("{}.qrk", self.quardle)) - .arg("alpine-minirootfs") - .output() - .unwrap(); - println!("{}.qrk has been created.", self.quardle); + println!("{} has been created.", quardle_name); + + // Clean temporary files and directories (keeping kaps and kernel beceause of their size) + remove_dir_all(ROOTFS_DIR).expect("Failed to delete rootfs directory"); + remove_dir_all(BUNDLE_DIR).expect("Failed to delete container bundle"); + remove_file(INITRAMFS_NAME).expect("Failed to delete initramfs image"); + remove_file(CONFIG_FILE).expect("Failed to delete json config file"); Ok(()) } } @@ -126,22 +140,24 @@ impl PreBuild { /// 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) { - if Path::new("kaps/target/release/kaps").exists() { + if Path::new("kaps/target/x86_64-unknown-linux-musl/release/kaps").exists() { println!("Kaps binary already exists, skipping."); return; } - println!("Removing old kaps folder"); + println!("Cloning the Kaps GitHub repository..."); + Repository::clone(kaps_repository_url, "kaps").unwrap(); + // 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("rm -rf kaps") + .arg("cd kaps && git checkout cdce0eb") .output() - .expect("failed to remove previous kaps folder"); - println!("Cloning kaps GitHub repository"); - Repository::clone(kaps_repository_url, "kaps").unwrap(); - println!("Building kaps binary"); + .expect("failed to checkout to working version of kaps"); + + println!("Building kaps binary..."); let stdout = Command::new("sh") .arg("-c") - .arg("cd kaps && cargo build --release") + .arg("cd kaps && cargo build --release --target=x86_64-unknown-linux-musl") .stdout(Stdio::piped()) .spawn() .unwrap() @@ -155,13 +171,13 @@ impl PreBuild { } /// Building kernel from cloud-hypervisor/linux Git repository, with a *x86_64* config. - /// If the `bzImage` exists, skipping this step. + /// If the `kernel` exists, skipping this step. pub fn build_kernel() { - if Path::new("linux-cloud-hypervisor/arch/x86/boot/bzImage").exists() { - println!("bzImage file already exists, no need to re-build the kernel, skipping."); + if Path::new(KERNEL_PATH).exists() { + println!("Kernel already exists, no need to re-build it, skipping."); return; } - println!("Building kernel"); + println!("Building kernel..."); let stdout = Command::new("bash") .arg("-c") .arg("kernel/mkkernel.sh") @@ -177,27 +193,89 @@ impl PreBuild { .for_each(|line| println!("{}", line)); } - /// 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. - /// If the folder exists, skipping this step. - pub fn build_fs() { - if Path::new("alpine-minirootfs").exists() { - println!("rootfs already exists, skipping."); + /// Creates the container bundle, containing the image inside a rootfs folder + /// and a runc spec (config.json). + pub fn build_bundle(image_url: &str) { + if Path::new(BUNDLE_DIR).exists() { + println!("Container bundle already exists, skipping."); return; } + println!("Creating container bundle..."); let stdout = Command::new("bash") .arg("-c") - .arg("rootfs/mkrootfs.sh") + .arg(format!("scripts/mkbundle.sh {}", image_url)) .stdout(Stdio::piped()) .spawn() - .unwrap() - .stdout .unwrap(); - let reader = BufReader::new(stdout); + let reader = BufReader::new(stdout.stdout.unwrap()); reader .lines() .filter_map(|line| line.ok()) .for_each(|line| println!("{}", line)); } + + /// 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) { + if Path::new(INITRAMFS_NAME).exists() { + println!("rootfs image already exists, skipping."); + return; + } + BufReader::new( + Command::new("bash") + .arg("-c") + .arg("scripts/mkrootfs.sh") + .stdout(Stdio::piped()) + .spawn() + .unwrap() + .stdout + .unwrap(), + ) + .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)).unwrap(); + + 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() + .unwrap() + .stdout + .unwrap(), + ) + .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() + .unwrap() + .stdout + .unwrap(), + ) + .lines() + .filter_map(|line| line.ok()) + .for_each(|line| println!("{}", line)); + } }