diff --git a/Cargo.lock b/Cargo.lock index fab1230..f0b262b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1451,6 +1451,7 @@ dependencies = [ "krata", "krata-oci", "krata-runtime", + "krata-tokio-tar", "log", "prost", "redb", diff --git a/crates/ctl/src/cli/launch.rs b/crates/ctl/src/cli/launch.rs index e65f968..01bfd8f 100644 --- a/crates/ctl/src/cli/launch.rs +++ b/crates/ctl/src/cli/launch.rs @@ -64,6 +64,10 @@ pub struct LauchCommand { help = "Wait for the guest to start, implied by --attach" )] wait: bool, + #[arg(short = 'k', long, help = "OCI kernel image for guest to use")] + kernel: Option, + #[arg(short = 'I', long, help = "OCI initrd image for guest to use")] + initrd: Option, #[arg(help = "Container image for guest to use")] oci: String, #[arg( @@ -80,27 +84,41 @@ impl LauchCommand { mut client: ControlServiceClient, events: EventStream, ) -> Result<()> { - let response = client - .pull_image(PullImageRequest { - image: self.oci.clone(), - format: match self.image_format { - LaunchImageFormat::Squashfs => OciImageFormat::Squashfs.into(), - LaunchImageFormat::Erofs => OciImageFormat::Erofs.into(), + let image = self + .pull_image( + &mut client, + &self.oci, + match self.image_format { + LaunchImageFormat::Squashfs => OciImageFormat::Squashfs, + LaunchImageFormat::Erofs => OciImageFormat::Erofs, }, - overwrite_cache: self.pull_overwrite_cache, - }) + ) .await?; - let reply = pull_interactive_progress(response.into_inner()).await?; + + let kernel = if let Some(ref kernel) = self.kernel { + let kernel_image = self + .pull_image(&mut client, kernel, OciImageFormat::Tar) + .await?; + Some(kernel_image) + } else { + None + }; + + let initrd = if let Some(ref initrd) = self.initrd { + let kernel_image = self + .pull_image(&mut client, initrd, OciImageFormat::Tar) + .await?; + Some(kernel_image) + } else { + None + }; let request = CreateGuestRequest { spec: Some(GuestSpec { name: self.name.unwrap_or_default(), - image: Some(GuestImageSpec { - image: Some(Image::Oci(GuestOciImageSpec { - digest: reply.digest, - format: reply.format, - })), - }), + image: Some(image), + kernel, + initrd, vcpus: self.cpus, mem: self.mem, task: Some(GuestTaskSpec { @@ -146,6 +164,28 @@ impl LauchCommand { StdioConsoleStream::restore_terminal_mode(); std::process::exit(code.unwrap_or(0)); } + + async fn pull_image( + &self, + client: &mut ControlServiceClient, + image: &str, + format: OciImageFormat, + ) -> Result { + let response = client + .pull_image(PullImageRequest { + image: image.to_string(), + format: format.into(), + overwrite_cache: self.pull_overwrite_cache, + }) + .await?; + let reply = pull_interactive_progress(response.into_inner()).await?; + Ok(GuestImageSpec { + image: Some(Image::Oci(GuestOciImageSpec { + digest: reply.digest, + format: reply.format, + })), + }) + } } async fn wait_guest_started(id: &str, events: EventStream) -> Result<()> { diff --git a/crates/daemon/Cargo.toml b/crates/daemon/Cargo.toml index 2f1c053..495d294 100644 --- a/crates/daemon/Cargo.toml +++ b/crates/daemon/Cargo.toml @@ -27,6 +27,7 @@ scopeguard = { workspace = true } signal-hook = { workspace = true } tokio = { workspace = true } tokio-stream = { workspace = true } +krata-tokio-tar = { workspace = true } tonic = { workspace = true, features = ["tls"] } uuid = { workspace = true } diff --git a/crates/daemon/src/control.rs b/crates/daemon/src/control.rs index 863aee7..f56bfda 100644 --- a/crates/daemon/src/control.rs +++ b/crates/daemon/src/control.rs @@ -378,7 +378,7 @@ impl ControlService for DaemonControlService { format: match packed.format { OciPackedFormat::Squashfs => OciImageFormat::Squashfs.into(), OciPackedFormat::Erofs => OciImageFormat::Erofs.into(), - _ => OciImageFormat::Unknown.into(), + OciPackedFormat::Tar => OciImageFormat::Tar.into(), }, }; yield reply; diff --git a/crates/daemon/src/lib.rs b/crates/daemon/src/lib.rs index 7e1023f..7df39a2 100644 --- a/crates/daemon/src/lib.rs +++ b/crates/daemon/src/lib.rs @@ -1,6 +1,6 @@ use std::{net::SocketAddr, path::PathBuf, str::FromStr}; -use anyhow::Result; +use anyhow::{anyhow, Result}; use console::{DaemonConsole, DaemonConsoleHandle}; use control::DaemonControlService; use db::GuestStore; @@ -74,9 +74,11 @@ impl Daemon { generated }; - let packer = OciPackerService::new(None, &image_cache_dir, OciPlatform::current()).await?; + let initrd_path = detect_guest_file(&store, "initrd")?; + let kernel_path = detect_guest_file(&store, "kernel")?; - let runtime = Runtime::new(store.clone()).await?; + let packer = OciPackerService::new(None, &image_cache_dir, OciPlatform::current()).await?; + let runtime = Runtime::new().await?; let glt = GuestLookupTable::new(0, host_uuid); let guests_db_path = format!("{}/guests.db", store); let guests = GuestStore::open(&PathBuf::from(guests_db_path))?; @@ -97,6 +99,8 @@ impl Daemon { runtime_for_reconciler, packer.clone(), guest_reconciler_notify.clone(), + kernel_path, + initrd_path, )?; let guest_reconciler_task = guest_reconciler.launch(guest_reconciler_receiver).await?; @@ -181,3 +185,16 @@ impl Drop for Daemon { self.generator_task.abort(); } } + +fn detect_guest_file(store: &str, name: &str) -> Result { + let mut path = PathBuf::from(format!("{}/guest/{}", store, name)); + if path.is_file() { + return Ok(path); + } + + path = PathBuf::from(format!("/usr/share/krata/guest/{}", name)); + if path.is_file() { + return Ok(path); + } + Err(anyhow!("unable to find required guest file: {}", name)) +} diff --git a/crates/daemon/src/reconcile/guest.rs b/crates/daemon/src/reconcile/guest/mod.rs similarity index 78% rename from crates/daemon/src/reconcile/guest.rs rename to crates/daemon/src/reconcile/guest/mod.rs index c947b62..64fdc67 100644 --- a/crates/daemon/src/reconcile/guest.rs +++ b/crates/daemon/src/reconcile/guest/mod.rs @@ -1,20 +1,17 @@ use std::{ collections::{hash_map::Entry, HashMap}, + path::PathBuf, sync::Arc, time::Duration, }; -use anyhow::{anyhow, Result}; -use krata::launchcfg::LaunchPackedFormat; +use anyhow::Result; use krata::v1::{ - common::{ - guest_image_spec::Image, Guest, GuestErrorInfo, GuestExitInfo, GuestNetworkState, - GuestState, GuestStatus, OciImageFormat, - }, + common::{Guest, GuestErrorInfo, GuestExitInfo, GuestNetworkState, GuestState, GuestStatus}, control::GuestChangedEvent, }; -use krataoci::packer::{service::OciPackerService, OciPackedFormat}; -use kratart::{launch::GuestLaunchRequest, GuestInfo, Runtime}; +use krataoci::packer::service::OciPackerService; +use kratart::{GuestInfo, Runtime}; use log::{error, info, trace, warn}; use tokio::{ select, @@ -33,6 +30,10 @@ use crate::{ glt::GuestLookupTable, }; +use self::start::GuestStarter; + +mod start; + const PARALLEL_LIMIT: u32 = 5; #[derive(Debug)] @@ -59,12 +60,15 @@ pub struct GuestReconciler { events: DaemonEventContext, runtime: Runtime, packer: OciPackerService, + kernel_path: PathBuf, + initrd_path: PathBuf, tasks: Arc>>, guest_reconciler_notify: Sender, reconcile_lock: Arc>, } impl GuestReconciler { + #[allow(clippy::too_many_arguments)] pub fn new( glt: GuestLookupTable, guests: GuestStore, @@ -72,6 +76,8 @@ impl GuestReconciler { runtime: Runtime, packer: OciPackerService, guest_reconciler_notify: Sender, + kernel_path: PathBuf, + initrd_path: PathBuf, ) -> Result { Ok(Self { glt, @@ -79,6 +85,8 @@ impl GuestReconciler { events, runtime, packer, + kernel_path, + initrd_path, tasks: Arc::new(Mutex::new(HashMap::new())), guest_reconciler_notify, reconcile_lock: Arc::new(RwLock::with_max_readers((), PARALLEL_LIMIT)), @@ -246,76 +254,14 @@ impl GuestReconciler { } async fn start(&self, uuid: Uuid, guest: &mut Guest) -> Result { - let Some(ref spec) = guest.spec else { - return Err(anyhow!("guest spec not specified")); + let starter = GuestStarter { + kernel_path: &self.kernel_path, + initrd_path: &self.initrd_path, + packer: &self.packer, + glt: &self.glt, + runtime: &self.runtime, }; - - let Some(ref image) = spec.image else { - return Err(anyhow!("image spec not provided")); - }; - let oci = match image.image { - Some(Image::Oci(ref oci)) => oci, - None => { - return Err(anyhow!("oci spec not specified")); - } - }; - let task = spec.task.as_ref().cloned().unwrap_or_default(); - - let image = self - .packer - .recall( - &oci.digest, - match oci.format() { - OciImageFormat::Unknown => OciPackedFormat::Squashfs, - OciImageFormat::Squashfs => OciPackedFormat::Squashfs, - OciImageFormat::Erofs => OciPackedFormat::Erofs, - OciImageFormat::Tar => { - return Err(anyhow!("tar image format is not supported for guests")); - } - }, - ) - .await?; - - let Some(image) = image else { - return Err(anyhow!( - "image {} in the requested format did not exist", - oci.digest - )); - }; - - let info = self - .runtime - .launch(GuestLaunchRequest { - format: LaunchPackedFormat::Squashfs, - uuid: Some(uuid), - name: if spec.name.is_empty() { - None - } else { - Some(spec.name.clone()) - }, - image, - vcpus: spec.vcpus, - mem: spec.mem, - env: task - .environment - .iter() - .map(|x| (x.key.clone(), x.value.clone())) - .collect::>(), - run: empty_vec_optional(task.command.clone()), - debug: false, - }) - .await?; - self.glt.associate(uuid, info.domid).await; - info!("started guest {}", uuid); - guest.state = Some(GuestState { - status: GuestStatus::Started.into(), - network: Some(guestinfo_to_networkstate(&info)), - exit_info: None, - error_info: None, - host: self.glt.host_uuid().to_string(), - domid: info.domid, - }); - Ok(GuestReconcilerResult::Changed { rerun: false }) + starter.start(uuid, guest).await } async fn exited(&self, guest: &mut Guest) -> Result { @@ -390,15 +336,7 @@ impl GuestReconciler { } } -fn empty_vec_optional(value: Vec) -> Option> { - if value.is_empty() { - None - } else { - Some(value) - } -} - -fn guestinfo_to_networkstate(info: &GuestInfo) -> GuestNetworkState { +pub fn guestinfo_to_networkstate(info: &GuestInfo) -> GuestNetworkState { GuestNetworkState { guest_ipv4: info.guest_ipv4.map(|x| x.to_string()).unwrap_or_default(), guest_ipv6: info.guest_ipv6.map(|x| x.to_string()).unwrap_or_default(), diff --git a/crates/daemon/src/reconcile/guest/start.rs b/crates/daemon/src/reconcile/guest/start.rs new file mode 100644 index 0000000..92bf490 --- /dev/null +++ b/crates/daemon/src/reconcile/guest/start.rs @@ -0,0 +1,182 @@ +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +use anyhow::{anyhow, Result}; +use futures::StreamExt; +use krata::launchcfg::LaunchPackedFormat; +use krata::v1::common::GuestOciImageSpec; +use krata::v1::common::{guest_image_spec::Image, Guest, GuestState, GuestStatus, OciImageFormat}; +use krataoci::packer::{service::OciPackerService, OciPackedFormat}; +use kratart::{launch::GuestLaunchRequest, Runtime}; +use log::info; + +use tokio::fs::{self, File}; +use tokio::io::AsyncReadExt; +use tokio_tar::Archive; +use uuid::Uuid; + +use crate::{ + glt::GuestLookupTable, + reconcile::guest::{guestinfo_to_networkstate, GuestReconcilerResult}, +}; + +// if a kernel is >= 100MB, that's kinda scary. +const OCI_SPEC_TAR_FILE_MAX_SIZE: usize = 100 * 1024 * 1024; + +pub struct GuestStarter<'a> { + pub kernel_path: &'a Path, + pub initrd_path: &'a Path, + pub packer: &'a OciPackerService, + pub glt: &'a GuestLookupTable, + pub runtime: &'a Runtime, +} + +impl GuestStarter<'_> { + pub async fn oci_spec_tar_read_file( + &self, + file: &Path, + oci: &GuestOciImageSpec, + ) -> Result> { + if oci.format() != OciImageFormat::Tar { + return Err(anyhow!( + "oci image spec for {} is required to be in tar format", + oci.digest + )); + } + + let image = self + .packer + .recall(&oci.digest, OciPackedFormat::Tar) + .await?; + + let Some(image) = image else { + return Err(anyhow!("image {} was not found in tar format", oci.digest)); + }; + + let mut archive = Archive::new(File::open(&image.path).await?); + let mut entries = archive.entries()?; + while let Some(entry) = entries.next().await { + let mut entry = entry?; + let path = entry.path()?; + if entry.header().size()? as usize > OCI_SPEC_TAR_FILE_MAX_SIZE { + return Err(anyhow!( + "file {} in image {} is larger than the size limit", + file.to_string_lossy(), + oci.digest + )); + } + if path == file { + let mut buffer = Vec::new(); + entry.read_to_end(&mut buffer).await?; + return Ok(buffer); + } + } + Err(anyhow!( + "unable to find file {} in image {}", + file.to_string_lossy(), + oci.digest + )) + } + + pub async fn start(&self, uuid: Uuid, guest: &mut Guest) -> Result { + let Some(ref spec) = guest.spec else { + return Err(anyhow!("guest spec not specified")); + }; + + let Some(ref image) = spec.image else { + return Err(anyhow!("image spec not provided")); + }; + let oci = match image.image { + Some(Image::Oci(ref oci)) => oci, + None => { + return Err(anyhow!("oci spec not specified")); + } + }; + let task = spec.task.as_ref().cloned().unwrap_or_default(); + + let image = self + .packer + .recall( + &oci.digest, + match oci.format() { + OciImageFormat::Unknown => OciPackedFormat::Squashfs, + OciImageFormat::Squashfs => OciPackedFormat::Squashfs, + OciImageFormat::Erofs => OciPackedFormat::Erofs, + OciImageFormat::Tar => { + return Err(anyhow!("tar image format is not supported for guests")); + } + }, + ) + .await?; + + let Some(image) = image else { + return Err(anyhow!( + "image {} in the requested format did not exist", + oci.digest + )); + }; + + let kernel = if let Some(ref spec) = spec.kernel { + let Some(Image::Oci(ref oci)) = spec.image else { + return Err(anyhow!("kernel image spec must be an oci image")); + }; + self.oci_spec_tar_read_file(&PathBuf::from("kernel/image"), oci) + .await? + } else { + fs::read(&self.kernel_path).await? + }; + let initrd = if let Some(ref spec) = spec.initrd { + let Some(Image::Oci(ref oci)) = spec.image else { + return Err(anyhow!("initrd image spec must be an oci image")); + }; + self.oci_spec_tar_read_file(&PathBuf::from("krata/initrd"), oci) + .await? + } else { + fs::read(&self.initrd_path).await? + }; + + let info = self + .runtime + .launch(GuestLaunchRequest { + format: LaunchPackedFormat::Squashfs, + uuid: Some(uuid), + name: if spec.name.is_empty() { + None + } else { + Some(spec.name.clone()) + }, + image, + kernel, + initrd, + vcpus: spec.vcpus, + mem: spec.mem, + env: task + .environment + .iter() + .map(|x| (x.key.clone(), x.value.clone())) + .collect::>(), + run: empty_vec_optional(task.command.clone()), + debug: false, + }) + .await?; + self.glt.associate(uuid, info.domid).await; + info!("started guest {}", uuid); + guest.state = Some(GuestState { + status: GuestStatus::Started.into(), + network: Some(guestinfo_to_networkstate(&info)), + exit_info: None, + error_info: None, + host: self.glt.host_uuid().to_string(), + domid: info.domid, + }); + Ok(GuestReconcilerResult::Changed { rerun: false }) + } +} + +fn empty_vec_optional(value: Vec) -> Option> { + if value.is_empty() { + None + } else { + Some(value) + } +} diff --git a/crates/krata/proto/krata/v1/common.proto b/crates/krata/proto/krata/v1/common.proto index 84bd7f9..0b07c46 100644 --- a/crates/krata/proto/krata/v1/common.proto +++ b/crates/krata/proto/krata/v1/common.proto @@ -17,10 +17,14 @@ message Guest { message GuestSpec { string name = 1; GuestImageSpec image = 2; - uint32 vcpus = 3; - uint64 mem = 4; - GuestTaskSpec task = 5; - repeated GuestSpecAnnotation annotations = 6; + // If not specified, defaults to the daemon default kernel. + GuestImageSpec kernel = 3; + // If not specified, defaults to the daemon default initrd. + GuestImageSpec initrd = 4; + uint32 vcpus = 5; + uint64 mem = 6; + GuestTaskSpec task = 7; + repeated GuestSpecAnnotation annotations = 8; } message GuestImageSpec { diff --git a/crates/oci/src/fetch.rs b/crates/oci/src/fetch.rs index fc4ce4a..1795f12 100644 --- a/crates/oci/src/fetch.rs +++ b/crates/oci/src/fetch.rs @@ -217,6 +217,13 @@ impl OciImageFetcher { continue; } } + + if let Some(ref digest) = image.digest { + if digest != manifest.digest() { + continue; + } + } + found = Some(manifest); break; } @@ -240,7 +247,7 @@ impl OciImageFetcher { let mut client = OciRegistryClient::new(image.registry_url()?, self.platform.clone())?; let (manifest, descriptor, digest) = client - .get_manifest_with_digest(&image.name, &image.reference) + .get_manifest_with_digest(&image.name, image.reference.as_ref(), image.digest.as_ref()) .await?; let descriptor = descriptor.unwrap_or_else(|| { DescriptorBuilder::default() diff --git a/crates/oci/src/name.rs b/crates/oci/src/name.rs index ab92da1..5babe71 100644 --- a/crates/oci/src/name.rs +++ b/crates/oci/src/name.rs @@ -2,33 +2,39 @@ use anyhow::Result; use std::fmt; use url::Url; -const DOCKER_HUB_MIRROR: &str = "mirror.gcr.io"; -const DEFAULT_IMAGE_TAG: &str = "latest"; - #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ImageName { pub hostname: String, pub port: Option, pub name: String, - pub reference: String, + pub reference: Option, + pub digest: Option, } impl fmt::Display for ImageName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if DOCKER_HUB_MIRROR == self.hostname && self.port.is_none() { + let mut suffix = String::new(); + + if let Some(ref reference) = self.reference { + suffix.push(':'); + suffix.push_str(reference); + } + + if let Some(ref digest) = self.digest { + suffix.push('@'); + suffix.push_str(digest); + } + + if ImageName::DOCKER_HUB_MIRROR == self.hostname && self.port.is_none() { if self.name.starts_with("library/") { - write!(f, "{}:{}", &self.name[8..], self.reference) + write!(f, "{}{}", &self.name[8..], suffix) } else { - write!(f, "{}:{}", self.name, self.reference) + write!(f, "{}{}", self.name, suffix) } } else if let Some(port) = self.port { - write!( - f, - "{}:{}/{}:{}", - self.hostname, port, self.name, self.reference - ) + write!(f, "{}:{}/{}{}", self.hostname, port, self.name, suffix) } else { - write!(f, "{}/{}:{}", self.hostname, self.name, self.reference) + write!(f, "{}/{}{}", self.hostname, self.name, suffix) } } } @@ -41,13 +47,21 @@ impl Default for ImageName { } impl ImageName { + pub const DOCKER_HUB_MIRROR: &'static str = "registry.docker.io"; + pub const DEFAULT_IMAGE_TAG: &'static str = "latest"; + pub fn parse(name: &str) -> Result { let full_name = name.to_string(); let name = full_name.clone(); let (mut hostname, mut name) = name .split_once('/') .map(|x| (x.0.to_string(), x.1.to_string())) - .unwrap_or_else(|| (DOCKER_HUB_MIRROR.to_string(), format!("library/{}", name))); + .unwrap_or_else(|| { + ( + ImageName::DOCKER_HUB_MIRROR.to_string(), + format!("library/{}", name), + ) + }); // heuristic to find any docker hub image formats // that may be in the hostname format. for example: @@ -55,7 +69,7 @@ impl ImageName { // and neither will abc/hello/xyz:latest if !hostname.contains('.') && full_name.chars().filter(|x| *x == '/').count() == 1 { name = format!("{}/{}", hostname, name); - hostname = DOCKER_HUB_MIRROR.to_string(); + hostname = ImageName::DOCKER_HUB_MIRROR.to_string(); } let (hostname, port) = if let Some((hostname, port)) = hostname @@ -66,15 +80,54 @@ impl ImageName { } else { (hostname, None) }; - let (name, reference) = name - .split_once(':') - .map(|x| (x.0.to_string(), x.1.to_string())) - .unwrap_or((name.to_string(), DEFAULT_IMAGE_TAG.to_string())); + + let name_has_digest = if name.contains('@') { + let digest_start = name.chars().position(|c| c == '@'); + let ref_start = name.chars().position(|c| c == ':'); + if let (Some(digest_start), Some(ref_start)) = (digest_start, ref_start) { + digest_start < ref_start + } else { + true + } + } else { + false + }; + + let (name, digest) = if name_has_digest { + name.split_once('@') + .map(|(name, digest)| (name.to_string(), Some(digest.to_string()))) + .unwrap_or_else(|| (name, None)) + } else { + (name, None) + }; + + let (name, reference) = if name.contains(':') { + name.split_once(':') + .map(|(name, reference)| (name.to_string(), Some(reference.to_string()))) + .unwrap_or((name, None)) + } else { + (name, None) + }; + + let (reference, digest) = if let Some(reference) = reference { + if let Some(digest) = digest { + (Some(reference), Some(digest)) + } else { + reference + .split_once('@') + .map(|(reff, digest)| (Some(reff.to_string()), Some(digest.to_string()))) + .unwrap_or_else(|| (Some(reference), None)) + } + } else { + (None, digest) + }; + Ok(ImageName { hostname, port, name, reference, + digest, }) } diff --git a/crates/oci/src/packer/cache.rs b/crates/oci/src/packer/cache.rs index 32743c1..00b020b 100644 --- a/crates/oci/src/packer/cache.rs +++ b/crates/oci/src/packer/cache.rs @@ -159,7 +159,9 @@ impl OciPackerCache { let image_name = packed.name.to_string(); annotations.insert(ANNOTATION_IMAGE_NAME.to_string(), image_name); let image_ref = packed.name.reference.clone(); - annotations.insert(ANNOTATION_REF_NAME.to_string(), image_ref); + if let Some(image_ref) = image_ref { + annotations.insert(ANNOTATION_REF_NAME.to_string(), image_ref); + } descriptor.set_annotations(Some(annotations)); manifests.push(descriptor.clone()); index.set_manifests(manifests); diff --git a/crates/oci/src/packer/service.rs b/crates/oci/src/packer/service.rs index 9fff54b..8e563a1 100644 --- a/crates/oci/src/packer/service.rs +++ b/crates/oci/src/packer/service.rs @@ -61,6 +61,10 @@ impl OciPackerService { digest: &str, format: OciPackedFormat, ) -> Result> { + if digest.contains('/') || digest.contains('\\') || digest.contains("..") { + return Ok(None); + } + self.cache .recall(ImageName::parse("cached:latest")?, digest, format) .await diff --git a/crates/oci/src/registry.rs b/crates/oci/src/registry.rs index 1874ae8..4e4a857 100644 --- a/crates/oci/src/registry.rs +++ b/crates/oci/src/registry.rs @@ -7,7 +7,7 @@ use reqwest::{Client, RequestBuilder, Response, StatusCode}; use tokio::{fs::File, io::AsyncWriteExt}; use url::Url; -use crate::{progress::OciBoundProgress, schema::OciSchema}; +use crate::{name::ImageName, progress::OciBoundProgress, schema::OciSchema}; #[derive(Clone, Debug)] pub struct OciPlatform { @@ -176,7 +176,7 @@ impl OciRegistryClient { let url = self.url.join(&format!( "/v2/{}/manifests/{}", name.as_ref(), - reference.as_ref() + reference.as_ref(), ))?; let accept = format!( "{}, {}, {}, {}", @@ -202,13 +202,20 @@ impl OciRegistryClient { pub async fn get_manifest_with_digest, R: AsRef>( &mut self, name: N, - reference: R, + reference: Option, + digest: Option, ) -> Result<(OciSchema, Option, String)> { - let url = self.url.join(&format!( - "/v2/{}/manifests/{}", - name.as_ref(), - reference.as_ref() - ))?; + let what = digest + .as_ref() + .map(|x| x.as_ref().to_string()) + .unwrap_or_else(|| { + reference + .map(|x| x.as_ref().to_string()) + .unwrap_or_else(|| ImageName::DEFAULT_IMAGE_TAG.to_string()) + }); + let url = self + .url + .join(&format!("/v2/{}/manifests/{}", name.as_ref(), what,))?; let accept = format!( "{}, {}, {}, {}", MediaType::ImageManifest.to_docker_v2s2()?, @@ -239,9 +246,10 @@ impl OciRegistryClient { let digest = response .headers() .get("Docker-Content-Digest") - .ok_or_else(|| anyhow!("fetching manifest did not yield a content digest"))? - .to_str()? - .to_string(); + .and_then(|x| x.to_str().ok()) + .map(|x| x.to_string()) + .or_else(|| digest.map(|x: N| x.as_ref().to_string())) + .ok_or_else(|| anyhow!("fetching manifest did not yield a content digest"))?; let bytes = response.bytes().await?; let manifest = serde_json::from_slice(&bytes)?; Ok((OciSchema::new(bytes.to_vec(), manifest), None, digest)) diff --git a/crates/runtime/src/launch.rs b/crates/runtime/src/launch.rs index e76f892..ca4d8b5 100644 --- a/crates/runtime/src/launch.rs +++ b/crates/runtime/src/launch.rs @@ -23,6 +23,8 @@ use super::{GuestInfo, GuestState}; pub struct GuestLaunchRequest { pub format: LaunchPackedFormat, + pub kernel: Vec, + pub initrd: Vec, pub uuid: Option, pub name: Option, pub vcpus: u32, @@ -173,22 +175,22 @@ impl GuestLauncher { let config = DomainConfig { backend_domid: 0, - name: &xen_name, + name: xen_name, max_vcpus: request.vcpus, mem_mb: request.mem, - kernel_path: &context.kernel, - initrd_path: &context.initrd, - cmdline: &cmdline, - use_console_backend: Some("krata-console"), + kernel: request.kernel, + initrd: request.initrd, + cmdline, + use_console_backend: Some("krata-console".to_string()), disks: vec![ DomainDisk { - vdev: "xvda", - block: &image_squashfs_loop, + vdev: "xvda".to_string(), + block: image_squashfs_loop.clone(), writable: false, }, DomainDisk { - vdev: "xvdb", - block: &cfgblk_squashfs_loop, + vdev: "xvdb".to_string(), + block: cfgblk_squashfs_loop.clone(), writable: false, }, ], @@ -197,7 +199,7 @@ impl GuestLauncher { initialized: false, }], vifs: vec![DomainNetworkInterface { - mac: &guest_mac_string, + mac: guest_mac_string.clone(), mtu: 1500, bridge: None, script: None, diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index 3d0f3d3..288da31 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -1,9 +1,4 @@ -use std::{ - fs, - path::{Path, PathBuf}, - str::FromStr, - sync::Arc, -}; +use std::{fs, path::PathBuf, str::FromStr, sync::Arc}; use anyhow::{anyhow, Result}; use ipnetwork::IpNetwork; @@ -52,43 +47,17 @@ pub struct GuestInfo { pub struct RuntimeContext { pub autoloop: AutoLoop, pub xen: XenClient, - pub kernel: String, - pub initrd: String, } impl RuntimeContext { - pub async fn new(store: String) -> Result { - let mut image_cache_path = PathBuf::from(&store); - image_cache_path.push("cache"); - fs::create_dir_all(&image_cache_path)?; - + pub async fn new() -> Result { let xen = XenClient::open(0).await?; - image_cache_path.push("image"); - fs::create_dir_all(&image_cache_path)?; - let kernel = RuntimeContext::detect_guest_file(&store, "kernel")?; - let initrd = RuntimeContext::detect_guest_file(&store, "initrd")?; - Ok(RuntimeContext { autoloop: AutoLoop::new(LoopControl::open()?), xen, - kernel, - initrd, }) } - fn detect_guest_file(store: &str, name: &str) -> Result { - let mut path = PathBuf::from(format!("{}/guest/{}", store, name)); - if path.is_file() { - return path_as_string(&path); - } - - path = PathBuf::from(format!("/usr/share/krata/guest/{}", name)); - if path.is_file() { - return path_as_string(&path); - } - Err(anyhow!("unable to find required guest file: {}", name)) - } - pub async fn list(&self) -> Result> { let mut guests: Vec = Vec::new(); for domid_candidate in self.xen.store.list("/local/domain").await? { @@ -248,16 +217,14 @@ impl RuntimeContext { #[derive(Clone)] pub struct Runtime { - store: Arc, context: RuntimeContext, launch_semaphore: Arc, } impl Runtime { - pub async fn new(store: String) -> Result { - let context = RuntimeContext::new(store.clone()).await?; + pub async fn new() -> Result { + let context = RuntimeContext::new().await?; Ok(Self { - store: Arc::new(store), context, launch_semaphore: Arc::new(Semaphore::new(1)), }) @@ -320,12 +287,6 @@ impl Runtime { } pub async fn dupe(&self) -> Result { - Runtime::new((*self.store).clone()).await + Runtime::new().await } } - -fn path_as_string(path: &Path) -> Result { - path.to_str() - .ok_or_else(|| anyhow!("unable to convert path to string")) - .map(|x| x.to_string()) -} diff --git a/crates/xen/xenclient/examples/boot.rs b/crates/xen/xenclient/examples/boot.rs index b8357dc..ef5031a 100644 --- a/crates/xen/xenclient/examples/boot.rs +++ b/crates/xen/xenclient/examples/boot.rs @@ -1,4 +1,5 @@ use std::{env, process}; +use tokio::fs; use xenclient::error::Result; use xenclient::{DomainConfig, XenClient}; @@ -16,12 +17,12 @@ async fn main() -> Result<()> { let client = XenClient::open(0).await?; let config = DomainConfig { backend_domid: 0, - name: "xenclient-test", + name: "xenclient-test".to_string(), max_vcpus: 1, mem_mb: 512, - kernel_path: kernel_image_path.as_str(), - initrd_path: initrd_path.as_str(), - cmdline: "debug elevator=noop", + kernel: fs::read(&kernel_image_path).await?, + initrd: fs::read(&initrd_path).await?, + cmdline: "debug elevator=noop".to_string(), use_console_backend: None, disks: vec![], channels: vec![], diff --git a/crates/xen/xenclient/src/elfloader.rs b/crates/xen/xenclient/src/elfloader.rs index 7822ea9..42d3194 100644 --- a/crates/xen/xenclient/src/elfloader.rs +++ b/crates/xen/xenclient/src/elfloader.rs @@ -107,17 +107,15 @@ impl ElfImageLoader { ElfImageLoader::load_xz(file.as_slice()) } - pub fn load_file_kernel(path: &str) -> Result { - let file = std::fs::read(path)?; - - for start in find_iter(file.as_slice(), &[0x1f, 0x8b]) { - if let Ok(elf) = ElfImageLoader::load_gz(&file[start..]) { + pub fn load_file_kernel(data: &[u8]) -> Result { + for start in find_iter(data, &[0x1f, 0x8b]) { + if let Ok(elf) = ElfImageLoader::load_gz(&data[start..]) { return Ok(elf); } } - for start in find_iter(file.as_slice(), &[0xfd, 0x37, 0x7a, 0x58]) { - if let Ok(elf) = ElfImageLoader::load_xz(&file[start..]) { + for start in find_iter(data, &[0xfd, 0x37, 0x7a, 0x58]) { + if let Ok(elf) = ElfImageLoader::load_xz(&data[start..]) { return Ok(elf); } } diff --git a/crates/xen/xenclient/src/lib.rs b/crates/xen/xenclient/src/lib.rs index dd7870b..4b69876 100644 --- a/crates/xen/xenclient/src/lib.rs +++ b/crates/xen/xenclient/src/lib.rs @@ -23,7 +23,6 @@ use boot::BootState; use log::{debug, trace, warn}; use tokio::time::timeout; -use std::fs::read; use std::path::PathBuf; use std::str::FromStr; use std::time::Duration; @@ -40,60 +39,60 @@ pub struct XenClient { call: XenCall, } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct BlockDeviceRef { pub path: String, pub major: u32, pub minor: u32, } -#[derive(Debug)] -pub struct DomainDisk<'a> { - pub vdev: &'a str, - pub block: &'a BlockDeviceRef, +#[derive(Clone, Debug)] +pub struct DomainDisk { + pub vdev: String, + pub block: BlockDeviceRef, pub writable: bool, } -#[derive(Debug)] -pub struct DomainFilesystem<'a> { - pub path: &'a str, - pub tag: &'a str, +#[derive(Clone, Debug)] +pub struct DomainFilesystem { + pub path: String, + pub tag: String, } -#[derive(Debug)] -pub struct DomainNetworkInterface<'a> { - pub mac: &'a str, +#[derive(Clone, Debug)] +pub struct DomainNetworkInterface { + pub mac: String, pub mtu: u32, - pub bridge: Option<&'a str>, - pub script: Option<&'a str>, + pub bridge: Option, + pub script: Option, } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct DomainChannel { pub typ: String, pub initialized: bool, } -#[derive(Debug)] -pub struct DomainEventChannel<'a> { - pub name: &'a str, +#[derive(Clone, Debug)] +pub struct DomainEventChannel { + pub name: String, } -#[derive(Debug)] -pub struct DomainConfig<'a> { +#[derive(Clone, Debug)] +pub struct DomainConfig { pub backend_domid: u32, - pub name: &'a str, + pub name: String, pub max_vcpus: u32, pub mem_mb: u64, - pub kernel_path: &'a str, - pub initrd_path: &'a str, - pub cmdline: &'a str, - pub disks: Vec>, - pub use_console_backend: Option<&'a str>, + pub kernel: Vec, + pub initrd: Vec, + pub cmdline: String, + pub disks: Vec, + pub use_console_backend: Option, pub channels: Vec, - pub vifs: Vec>, - pub filesystems: Vec>, - pub event_channels: Vec>, + pub vifs: Vec, + pub filesystems: Vec, + pub event_channels: Vec, pub extra_keys: Vec<(String, String)>, pub extra_rw_paths: Vec, } @@ -117,7 +116,7 @@ impl XenClient { Ok(XenClient { store, call }) } - pub async fn create(&self, config: &DomainConfig<'_>) -> Result { + pub async fn create(&self, config: &DomainConfig) -> Result { let mut domain = CreateDomain { max_vcpus: config.max_vcpus, ..Default::default() @@ -143,7 +142,7 @@ impl XenClient { &self, domid: u32, domain: &CreateDomain, - config: &DomainConfig<'_>, + config: &DomainConfig, ) -> Result { trace!( "XenClient init domid={} domain={:?} config={:?}", @@ -237,9 +236,9 @@ impl XenClient { &Uuid::from_bytes(domain.handle).to_string(), ) .await?; - tx.write_string(format!("{}/name", dom_path).as_str(), config.name) + tx.write_string(format!("{}/name", dom_path).as_str(), &config.name) .await?; - tx.write_string(format!("{}/name", vm_path).as_str(), config.name) + tx.write_string(format!("{}/name", vm_path).as_str(), &config.name) .await?; for (key, value) in &config.extra_keys { @@ -257,7 +256,7 @@ impl XenClient { self.call.set_max_vcpus(domid, config.max_vcpus).await?; self.call.set_max_mem(domid, config.mem_mb * 1024).await?; - let image_loader = ElfImageLoader::load_file_kernel(config.kernel_path)?; + let image_loader = ElfImageLoader::load_file_kernel(&config.kernel)?; let xenstore_evtchn: u32; let xenstore_mfn: u64; @@ -270,18 +269,17 @@ impl XenClient { let mut arch = Box::new(X86BootSetup::new()) as Box; #[cfg(target_arch = "aarch64")] let mut arch = Box::new(Arm64BootSetup::new()) as Box; - let initrd = read(config.initrd_path)?; state = boot .initialize( &mut arch, &image_loader, - initrd.as_slice(), + &config.initrd, config.max_vcpus, config.mem_mb, 1, ) .await?; - boot.boot(&mut arch, &mut state, config.cmdline).await?; + boot.boot(&mut arch, &mut state, &config.cmdline).await?; xenstore_evtchn = state.store_evtchn; xenstore_mfn = boot.phys.p2m[state.xenstore_segment.pfn as usize]; p2m = boot.phys.p2m; @@ -291,19 +289,9 @@ impl XenClient { let tx = self.store.transaction().await?; tx.write_string(format!("{}/image/os_type", vm_path).as_str(), "linux") .await?; - tx.write_string( - format!("{}/image/kernel", vm_path).as_str(), - config.kernel_path, - ) - .await?; - tx.write_string( - format!("{}/image/ramdisk", vm_path).as_str(), - config.initrd_path, - ) - .await?; tx.write_string( format!("{}/image/cmdline", vm_path).as_str(), - config.cmdline, + &config.cmdline, ) .await?; @@ -352,7 +340,8 @@ impl XenClient { &DomainChannel { typ: config .use_console_backend - .unwrap_or("xenconsoled") + .clone() + .unwrap_or("xenconsoled".to_string()) .to_string(), initialized: true, }, @@ -429,7 +418,7 @@ impl XenClient { .await?; let channel_path = format!("{}/evtchn/{}", dom_path, channel.name); self.store - .write_string(&format!("{}/name", channel_path), channel.name) + .write_string(&format!("{}/name", channel_path), &channel.name) .await?; self.store .write_string(&format!("{}/channel", channel_path), &id.to_string()) @@ -447,7 +436,7 @@ impl XenClient { backend_domid: u32, domid: u32, index: usize, - disk: &DomainDisk<'_>, + disk: &DomainDisk, ) -> Result<()> { let id = (202 << 8) | (index << 4) as u64; let backend_items: Vec<(&str, String)> = vec![ @@ -567,7 +556,7 @@ impl XenClient { backend_domid: u32, domid: u32, index: usize, - filesystem: &DomainFilesystem<'_>, + filesystem: &DomainFilesystem, ) -> Result<()> { let id = 90 + index as u64; let backend_items: Vec<(&str, String)> = vec![ @@ -605,7 +594,7 @@ impl XenClient { backend_domid: u32, domid: u32, index: usize, - vif: &DomainNetworkInterface<'_>, + vif: &DomainNetworkInterface, ) -> Result<()> { let id = 20 + index as u64; let mut backend_items: Vec<(&str, String)> = vec![ @@ -619,12 +608,12 @@ impl XenClient { ]; if vif.bridge.is_some() { - backend_items.extend_from_slice(&[("bridge", vif.bridge.unwrap().to_string())]); + backend_items.extend_from_slice(&[("bridge", vif.bridge.clone().unwrap())]); } if vif.script.is_some() { backend_items.extend_from_slice(&[ - ("script", vif.script.unwrap().to_string()), + ("script", vif.script.clone().unwrap()), ("hotplug-status", "".to_string()), ]); } else {