From 1c92ba54f3d0897963da0565b36fd289c5b2ed60 Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Thu, 18 Jan 2024 00:02:21 -0800 Subject: [PATCH] hypha: implement image cache --- hypha/Cargo.toml | 4 +++ hypha/bin/controller.rs | 30 ++++++++++++++++++--- hypha/src/ctl/mod.rs | 26 +++++++++++++----- hypha/src/error.rs | 6 +++++ hypha/src/image/cache.rs | 51 +++++++++++++++++++++++++++++++++++ hypha/src/image/mod.rs | 57 ++++++++++++++++++++++++++++++---------- 6 files changed, 149 insertions(+), 25 deletions(-) create mode 100644 hypha/src/image/cache.rs diff --git a/hypha/Cargo.toml b/hypha/Cargo.toml index cec2f6b..11f905d 100644 --- a/hypha/Cargo.toml +++ b/hypha/Cargo.toml @@ -12,7 +12,11 @@ log = "0.4.20" env_logger = "0.10.1" flate2 = "1.0.28" tar = "0.4.40" +directories = "5.0.1" walkdir = "2" +serde = "1.0.195" +serde_json = "1.0.111" +sha256 = "1.5.0" [dependencies.clap] version = "4.4.18" diff --git a/hypha/bin/controller.rs b/hypha/bin/controller.rs index 5342072..e87c947 100644 --- a/hypha/bin/controller.rs +++ b/hypha/bin/controller.rs @@ -1,6 +1,6 @@ use clap::Parser; use hypha::ctl::Controller; -use hypha::error::Result; +use hypha::error::{HyphaError, Result}; #[derive(Parser, Debug)] #[command(version, about)] @@ -19,16 +19,38 @@ struct ControllerArgs { #[arg(short, long, default_value_t = 512)] mem: u64, + + #[arg(short = 'C', long, default_value = "auto")] + cache: String, } fn main() -> Result<()> { env_logger::init(); let args = ControllerArgs::parse(); - let mut controller = - Controller::new(args.kernel, args.initrd, args.image, args.cpus, args.mem)?; - controller.compile()?; + let cache_path = if args.cache == "auto" { + default_cache_path() + .ok_or_else(|| HyphaError::new("unable to determine default cache path")) + } else { + Ok(args.cache) + }?; + + let mut controller = Controller::new( + cache_path, + args.kernel, + args.initrd, + args.image, + args.cpus, + args.mem, + )?; let domid = controller.launch()?; println!("launched domain: {}", domid); Ok(()) } + +fn default_cache_path() -> Option { + let user_dirs = directories::UserDirs::new()?; + let mut path = user_dirs.home_dir().to_path_buf(); + path.push(".hypha/cache"); + Some(path.to_str()?.to_string()) +} diff --git a/hypha/src/ctl/mod.rs b/hypha/src/ctl/mod.rs index 490495a..2383f5b 100644 --- a/hypha/src/ctl/mod.rs +++ b/hypha/src/ctl/mod.rs @@ -1,44 +1,56 @@ use crate::error::Result; -use crate::image::ImageCompiler; +use crate::image::cache::ImageCache; +use crate::image::{ImageCompiler, ImageInfo}; use ocipkg::ImageName; +use std::fs; +use std::path::PathBuf; use xenclient::{DomainConfig, XenClient}; pub struct Controller { + image_cache: ImageCache, + image: String, client: XenClient, kernel_path: String, initrd_path: String, vcpus: u32, mem: u64, - image: String, } impl Controller { pub fn new( + cache_path: String, kernel_path: String, initrd_path: String, image: String, vcpus: u32, mem: u64, ) -> Result { + fs::create_dir_all(&cache_path)?; + let client = XenClient::open()?; + let mut image_cache_path = PathBuf::from(cache_path); + image_cache_path.push("image"); + fs::create_dir_all(&image_cache_path)?; + let image_cache = ImageCache::new(&image_cache_path)?; Ok(Controller { + image_cache, + image, client, kernel_path, initrd_path, - image, vcpus, mem, }) } - pub fn compile(&mut self) -> Result<()> { + fn compile(&mut self) -> Result { let image = ImageName::parse(&self.image)?; - let compiler = ImageCompiler::new()?; - let _squashfs = compiler.compile(&image)?; - Ok(()) + let compiler = ImageCompiler::new(&self.image_cache)?; + compiler.compile(&image) } pub fn launch(&mut self) -> Result { + let _image_info = self.compile()?; let config = DomainConfig { max_vcpus: self.vcpus, mem_mb: self.mem, diff --git a/hypha/src/error.rs b/hypha/src/error.rs index 1cdbd2c..ea29cfa 100644 --- a/hypha/src/error.rs +++ b/hypha/src/error.rs @@ -66,3 +66,9 @@ impl From for HyphaError { HyphaError::new(value.to_string().as_str()) } } + +impl From for HyphaError { + fn from(value: serde_json::Error) -> Self { + HyphaError::new(value.to_string().as_str()) + } +} diff --git a/hypha/src/image/cache.rs b/hypha/src/image/cache.rs new file mode 100644 index 0000000..0a4702e --- /dev/null +++ b/hypha/src/image/cache.rs @@ -0,0 +1,51 @@ +use crate::image::{ImageInfo, Result}; +use log::debug; +use oci_spec::image::ImageManifest; +use std::fs; +use std::path::{Path, PathBuf}; + +pub struct ImageCache { + cache_dir: PathBuf, +} + +impl ImageCache { + pub fn new(cache_dir: &Path) -> Result { + Ok(ImageCache { + cache_dir: cache_dir.to_path_buf(), + }) + } + + pub fn recall(&self, digest: &str) -> Result> { + let mut squashfs_path = self.cache_dir.clone(); + let mut manifest_path = self.cache_dir.clone(); + squashfs_path.push(format!("{}.squashfs", digest)); + manifest_path.push(format!("{}.json", digest)); + Ok(if squashfs_path.exists() && manifest_path.exists() { + let squashfs_metadata = fs::metadata(&squashfs_path)?; + let manifest_metadata = fs::metadata(&manifest_path)?; + if squashfs_metadata.is_file() && manifest_metadata.is_file() { + let manifest_text = fs::read_to_string(&manifest_path)?; + let manifest: ImageManifest = serde_json::from_str(manifest_text.as_str())?; + debug!("cache hit digest={}", digest); + Some(ImageInfo::new(squashfs_path.clone(), manifest)?) + } else { + None + } + } else { + debug!("cache miss digest={}", digest); + None + }) + } + + pub fn store(&self, digest: &str, info: &ImageInfo) -> Result { + debug!("cache store digest={}", digest); + let mut squashfs_path = self.cache_dir.clone(); + let mut manifest_path = self.cache_dir.clone(); + squashfs_path.push(format!("{}.squashfs", digest)); + manifest_path.push(format!("{}.json", digest)); + fs::copy(&info.squashfs, &squashfs_path)?; + let manifest_text = serde_json::to_string_pretty(&info.manifest)?; + fs::write(&manifest_path, manifest_text)?; + ImageInfo::new(squashfs_path.clone(), info.manifest.clone()) + } +} diff --git a/hypha/src/image/mod.rs b/hypha/src/image/mod.rs index 4b09873..9dc581d 100644 --- a/hypha/src/image/mod.rs +++ b/hypha/src/image/mod.rs @@ -1,7 +1,10 @@ +pub mod cache; + use crate::error::{HyphaError, Result}; +use crate::image::cache::ImageCache; use backhand::{FilesystemWriter, NodeHeader}; use log::{debug, trace}; -use oci_spec::image::MediaType; +use oci_spec::image::{ImageManifest, MediaType}; use ocipkg::distribution::Client; use ocipkg::error::Error; use ocipkg::{Digest, ImageName}; @@ -13,14 +16,27 @@ use std::path::PathBuf; use uuid::Uuid; use walkdir::WalkDir; -pub struct ImageCompiler {} +pub struct ImageInfo { + pub squashfs: PathBuf, + pub manifest: ImageManifest, +} -impl ImageCompiler { - pub fn new() -> Result { - Ok(ImageCompiler {}) +impl ImageInfo { + fn new(squashfs: PathBuf, manifest: ImageManifest) -> Result { + Ok(ImageInfo { squashfs, manifest }) + } +} + +pub struct ImageCompiler<'a> { + cache: &'a ImageCache, +} + +impl ImageCompiler<'_> { + pub fn new(cache: &ImageCache) -> Result { + Ok(ImageCompiler { cache }) } - pub fn compile(&self, image: &ImageName) -> Result { + 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())); @@ -29,11 +45,17 @@ impl ImageCompiler { 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) + let info = self.download_and_compile(image, &image_dir, &squash_file)?; + fs::remove_dir_all(tmp_dir)?; + Ok(info) } - fn download(&self, image: &ImageName, image_dir: &PathBuf) -> Result<()> { + fn download_and_compile( + &self, + image: &ImageName, + image_dir: &PathBuf, + squash_file: &PathBuf, + ) -> Result { debug!( "ImageCompiler download image={image}, image_dir={}", image_dir.to_str().unwrap() @@ -43,12 +65,18 @@ impl ImageCompiler { } = image; let mut client = Client::new(image.registry_url()?, name.clone())?; let manifest = client.get_manifest(reference)?; + let manifest_serialized = serde_json::to_string(&manifest)?; + let manifest_digest = sha256::digest(manifest_serialized); + if let Some(cached) = self.cache.recall(&manifest_digest)? { + return Ok(cached); + } 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 => {} @@ -71,12 +99,14 @@ impl ImageCompiler { layer.digest(), layer.size() ); - return Ok(()); + self.squash(image_dir, squash_file)?; + let info = ImageInfo::new(squash_file.clone(), manifest.clone())?; + return self.cache.store(&manifest_digest, &info); } Err(Error::MissingLayer.into()) } - fn squash(&self, image_dir: &PathBuf, squash_file: &PathBuf) -> Result { + 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 { @@ -137,9 +167,8 @@ impl ImageCompiler { .ok_or_else(|| HyphaError::new("failed to convert squashfs string"))?; let mut out = File::create(squash_file)?; - trace!("ImageCompiler squash generateI : {}", squash_file_path); + trace!("ImageCompiler squash generate: {}", squash_file_path); writer.write(&mut out)?; - - Ok(squash_file_path.to_string()) + Ok(()) } }