diff --git a/hypha/Cargo.toml b/hypha/Cargo.toml index f83abb2..cec2f6b 100644 --- a/hypha/Cargo.toml +++ b/hypha/Cargo.toml @@ -9,18 +9,35 @@ path = "../xenclient" [dependencies] log = "0.4.20" +env_logger = "0.10.1" +flate2 = "1.0.28" +tar = "0.4.40" +walkdir = "2" [dependencies.clap] version = "4.4.18" features = ["derive"] +[dependencies.ocipkg] +version = "0.2.8" + +[dependencies.oci-spec] +version = "0.5.8" + +[dependencies.backhand] +version = "0.14.2" + +[dependencies.uuid] +version = "1.6.1" +features = ["v4"] + [lib] path = "src/lib.rs" [[bin]] -name = "hypha-agent" -path = "bin/agent.rs" +name = "hyphactl" +path = "bin/controller.rs" [[bin]] -name = "hypha-container" +name = "hyphad" path = "bin/container.rs" diff --git a/hypha/bin/agent.rs b/hypha/bin/agent.rs deleted file mode 100644 index 74365e4..0000000 --- a/hypha/bin/agent.rs +++ /dev/null @@ -1,27 +0,0 @@ -use clap::Parser; -use hypha::agent::Agent; -use hypha::error::Result; - -#[derive(Parser, Debug)] -#[command(version, about)] -struct AgentArgs { - #[arg(short, long)] - kernel: String, - - #[arg(short, long)] - initrd: String, - - #[arg(short, long, default_value_t = 1)] - cpus: u32, - - #[arg(short, long, default_value_t = 512)] - mem: u64, -} - -fn main() -> Result<()> { - let args = AgentArgs::parse(); - let mut agent = Agent::new(args.kernel, args.initrd, args.cpus, args.mem)?; - let domid = agent.launch()?; - println!("launched domain: {}", domid); - Ok(()) -} diff --git a/hypha/bin/container.rs b/hypha/bin/container.rs index ec04ad7..dc97f6d 100644 --- a/hypha/bin/container.rs +++ b/hypha/bin/container.rs @@ -1,5 +1,7 @@ use hypha::error::Result; fn main() -> Result<()> { + env_logger::init(); + Ok(()) } diff --git a/hypha/bin/controller.rs b/hypha/bin/controller.rs new file mode 100644 index 0000000..2f1b067 --- /dev/null +++ b/hypha/bin/controller.rs @@ -0,0 +1,39 @@ +use clap::Parser; +use hypha::ctl::Controller; +use hypha::error::Result; +use hypha::image::ImageCompiler; +use ocipkg::ImageName; + +#[derive(Parser, Debug)] +#[command(version, about)] +struct ControllerArgs { + #[arg(short, long)] + kernel: String, + + #[arg(short = 'r', long)] + initrd: String, + + #[arg(short, long)] + image: String, + + #[arg(short, long, default_value_t = 1)] + cpus: u32, + + #[arg(short, long, default_value_t = 512)] + mem: u64, +} + +fn main() -> Result<()> { + env_logger::init(); + + let args = ControllerArgs::parse(); + let mut controller = Controller::new(args.kernel, args.initrd, args.cpus, args.mem)?; + let image = ImageName::parse(args.image.as_str())?; + let compiler = ImageCompiler::new()?; + let squashfs = compiler.compile(&image)?; + println!("packed image into squashfs: {}", &squashfs); + + let domid = controller.launch()?; + println!("launched domain: {}", domid); + Ok(()) +} diff --git a/hypha/src/agent/mod.rs b/hypha/src/ctl/mod.rs similarity index 76% rename from hypha/src/agent/mod.rs rename to hypha/src/ctl/mod.rs index 1ecaf1c..366df9d 100644 --- a/hypha/src/agent/mod.rs +++ b/hypha/src/ctl/mod.rs @@ -1,7 +1,7 @@ use crate::error::Result; use xenclient::{DomainConfig, XenClient}; -pub struct Agent { +pub struct Controller { client: XenClient, kernel_path: String, initrd_path: String, @@ -9,10 +9,15 @@ pub struct Agent { mem: u64, } -impl Agent { - pub fn new(kernel_path: String, initrd_path: String, vcpus: u32, mem: u64) -> Result { +impl Controller { + pub fn new( + kernel_path: String, + initrd_path: String, + vcpus: u32, + mem: u64, + ) -> Result { let client = XenClient::open()?; - Ok(Agent { + Ok(Controller { client, kernel_path, initrd_path, diff --git a/hypha/src/error.rs b/hypha/src/error.rs index 04fee49..1cdbd2c 100644 --- a/hypha/src/error.rs +++ b/hypha/src/error.rs @@ -1,5 +1,7 @@ +use backhand::BackhandError; use std::error::Error; use std::fmt::{Display, Formatter}; +use std::path::StripPrefixError; use xenclient::XenClientError; pub type Result = std::result::Result; @@ -40,3 +42,27 @@ impl From for HyphaError { HyphaError::new(value.to_string().as_str()) } } + +impl From for HyphaError { + fn from(value: ocipkg::error::Error) -> Self { + HyphaError::new(value.to_string().as_str()) + } +} + +impl From for HyphaError { + fn from(value: walkdir::Error) -> Self { + HyphaError::new(value.to_string().as_str()) + } +} + +impl From for HyphaError { + fn from(value: StripPrefixError) -> Self { + HyphaError::new(value.to_string().as_str()) + } +} + +impl From for HyphaError { + fn from(value: BackhandError) -> Self { + HyphaError::new(value.to_string().as_str()) + } +} diff --git a/hypha/src/image/mod.rs b/hypha/src/image/mod.rs new file mode 100644 index 0000000..4b09873 --- /dev/null +++ b/hypha/src/image/mod.rs @@ -0,0 +1,145 @@ +use crate::error::{HyphaError, Result}; +use backhand::{FilesystemWriter, NodeHeader}; +use log::{debug, trace}; +use oci_spec::image::MediaType; +use ocipkg::distribution::Client; +use ocipkg::error::Error; +use ocipkg::{Digest, ImageName}; +use std::fs; +use std::fs::File; +use std::io::BufReader; +use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; +use std::path::PathBuf; +use uuid::Uuid; +use walkdir::WalkDir; + +pub struct ImageCompiler {} + +impl ImageCompiler { + pub fn new() -> Result { + Ok(ImageCompiler {}) + } + + pub fn compile(&self, image: &ImageName) -> Result { + debug!("ImageCompiler compile image={image}"); + let mut tmp_dir = std::env::temp_dir().clone(); + tmp_dir.push(format!("hypha-compile-{}", Uuid::new_v4())); + let mut image_dir = tmp_dir.clone(); + image_dir.push("image"); + fs::create_dir_all(&image_dir)?; + let mut squash_file = tmp_dir.clone(); + squash_file.push("image.squashfs"); + self.download(image, &image_dir)?; + self.squash(&image_dir, &squash_file) + } + + fn download(&self, image: &ImageName, image_dir: &PathBuf) -> Result<()> { + debug!( + "ImageCompiler download image={image}, image_dir={}", + image_dir.to_str().unwrap() + ); + let ImageName { + name, reference, .. + } = image; + let mut client = Client::new(image.registry_url()?, name.clone())?; + let manifest = client.get_manifest(reference)?; + for layer in manifest.layers() { + debug!( + "ImageCompiler download start digest={} size={}", + layer.digest(), + layer.size() + ); + let blob = client.get_blob(&Digest::new(layer.digest())?)?; + match layer.media_type() { + MediaType::ImageLayerGzip => {} + MediaType::Other(ty) => { + if !ty.ends_with("tar.gzip") { + continue; + } + } + _ => continue, + } + debug!( + "ImageCompiler download unpack digest={} size={}", + layer.digest(), + layer.size() + ); + let buf = flate2::read::GzDecoder::new(blob.as_slice()); + tar::Archive::new(buf).unpack(image_dir)?; + debug!( + "ImageCompiler download end digest={} size={}", + layer.digest(), + layer.size() + ); + return Ok(()); + } + Err(Error::MissingLayer.into()) + } + + fn squash(&self, image_dir: &PathBuf, squash_file: &PathBuf) -> Result { + let mut writer = FilesystemWriter::default(); + let walk = WalkDir::new(image_dir).follow_links(false); + for entry in walk { + let entry = entry?; + let rel = entry + .path() + .strip_prefix(image_dir)? + .to_str() + .ok_or_else(|| HyphaError::new("failed to strip prefix of tmpdir"))?; + let rel = format!("/{}", rel); + trace!("ImageCompiler squash write {}", rel); + let typ = entry.file_type(); + let metadata = fs::symlink_metadata(entry.path())?; + let uid = metadata.uid(); + let gid = metadata.gid(); + let mode = metadata.permissions().mode(); + let mtime = metadata.mtime(); + + if rel == "/" { + writer.set_root_uid(uid); + writer.set_root_gid(gid); + writer.set_root_mode(mode as u16); + continue; + } + + let header = NodeHeader { + permissions: mode as u16, + uid, + gid, + mtime: mtime as u32, + }; + if typ.is_symlink() { + let symlink = fs::read_link(entry.path())?; + let symlink = symlink + .to_str() + .ok_or_else(|| HyphaError::new("failed to read symlink"))?; + writer.push_symlink(symlink, rel, header)?; + } else if typ.is_dir() { + writer.push_dir(rel, header)?; + } else if typ.is_file() { + let reader = BufReader::new(File::open(entry.path())?); + writer.push_file(reader, rel, header)?; + } else if typ.is_block_device() { + let device = metadata.dev(); + writer.push_block_device(device as u32, rel, header)?; + } else if typ.is_char_device() { + let device = metadata.dev(); + writer.push_char_device(device as u32, rel, header)?; + } else { + return Err(HyphaError::new("invalid file type")); + } + } + + fs::remove_dir_all(image_dir)?; + + let squash_file_path = squash_file + .to_str() + .ok_or_else(|| HyphaError::new("failed to convert squashfs string"))?; + + let mut out = File::create(squash_file)?; + trace!("ImageCompiler squash generateI : {}", squash_file_path); + writer.write(&mut out)?; + + Ok(squash_file_path.to_string()) + } +} diff --git a/hypha/src/lib.rs b/hypha/src/lib.rs index 9ef2f72..16776c5 100644 --- a/hypha/src/lib.rs +++ b/hypha/src/lib.rs @@ -1,3 +1,4 @@ -pub mod agent; pub mod container; +pub mod ctl; pub mod error; +pub mod image;