diff --git a/Cargo.toml b/Cargo.toml index dc042e5..271503f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ rand = "0.8.5" rtnetlink = "0.14.1" serde_json = "1.0.113" sha256 = "1.5.0" +serde_yaml = "0.9" signal-hook = "0.3.17" slice-copy = "0.3.0" smoltcp = "0.11.0" diff --git a/crates/krata/proto/krata/control.proto b/crates/krata/proto/krata/control.proto index d0da3ec..d05604f 100644 --- a/crates/krata/proto/krata/control.proto +++ b/crates/krata/proto/krata/control.proto @@ -23,16 +23,18 @@ message GuestNetworkInfo { message GuestInfo { string id = 1; - GuestImageSpec image = 2; - GuestNetworkInfo network = 3; + string name = 2; + GuestImageSpec image = 3; + GuestNetworkInfo network = 4; } message LaunchGuestRequest { - GuestImageSpec image = 1; - uint32 vcpus = 2; - uint64 mem = 3; - repeated string env = 4; - repeated string run = 5; + string name = 1; + GuestImageSpec image = 2; + uint32 vcpus = 3; + uint64 mem = 4; + repeated string env = 5; + repeated string run = 6; } message LaunchGuestReply { diff --git a/crates/kratactl/bin/control.rs b/crates/kratactl/bin/control.rs index 875edc7..a3b1b75 100644 --- a/crates/kratactl/bin/control.rs +++ b/crates/kratactl/bin/control.rs @@ -22,6 +22,8 @@ struct ControllerArgs { enum Commands { List {}, Launch { + #[arg(short, long)] + name: Option, #[arg(short, long, default_value_t = 1)] cpus: u32, #[arg(short, long, default_value_t = 512)] @@ -59,6 +61,7 @@ async fn main() -> Result<()> { match args.command { Commands::Launch { + name, oci, cpus, mem, @@ -67,6 +70,7 @@ async fn main() -> Result<()> { run, } => { let request = LaunchGuestRequest { + name: name.unwrap_or_default(), image: Some(GuestImageSpec { image: Some(Image::Oci(GuestOciImageSpec { image: oci })), }), @@ -119,7 +123,7 @@ async fn main() -> Result<()> { .await? .into_inner(); let mut table = cli_tables::Table::new(); - let header = vec!["uuid", "ipv4", "ipv6", "image"]; + let header = vec!["name", "uuid", "ipv4", "ipv6", "image"]; table.push_row(&header)?; for guest in response.guests { let ipv4 = guest @@ -143,6 +147,7 @@ async fn main() -> Result<()> { }) .unwrap_or("unknown".to_string()); table.push_row_string(&vec![ + guest.name, guest.id, ipv4.to_string(), ipv6.to_string(), diff --git a/crates/kratad/Cargo.toml b/crates/kratad/Cargo.toml index 01689d5..d6de10a 100644 --- a/crates/kratad/Cargo.toml +++ b/crates/kratad/Cargo.toml @@ -14,6 +14,8 @@ futures = { workspace = true } krata = { path = "../krata" } kratart = { path = "../kratart" } log = { workspace = true } +serde = { workspace = true } +serde_yaml = { workspace = true } signal-hook = { workspace = true } tokio = { workspace = true } tokio-stream = { workspace = true } diff --git a/crates/kratad/bin/daemon.rs b/crates/kratad/bin/daemon.rs index 5948cc1..18aa163 100644 --- a/crates/kratad/bin/daemon.rs +++ b/crates/kratad/bin/daemon.rs @@ -4,6 +4,7 @@ use env_logger::Env; use krata::dial::ControlDialAddress; use kratad::Daemon; use kratart::Runtime; +use log::error; use std::{ str::FromStr, sync::{atomic::AtomicBool, Arc}, @@ -15,6 +16,8 @@ struct Args { listen: String, #[arg(short, long, default_value = "/var/lib/krata")] store: String, + #[arg(long, default_value = "false")] + no_load_guest_tab: bool, } #[tokio::main(flavor = "multi_thread", worker_threads = 10)] @@ -26,6 +29,11 @@ async fn main() -> Result<()> { let addr = ControlDialAddress::from_str(&args.listen)?; let runtime = Runtime::new(args.store.clone()).await?; let mut daemon = Daemon::new(args.store.clone(), runtime).await?; + if !args.no_load_guest_tab { + if let Err(error) = daemon.load_guest_tab().await { + error!("failed to load guest tab: {}", error); + } + } daemon.listen(addr).await?; Ok(()) } diff --git a/crates/kratad/src/control.rs b/crates/kratad/src/control.rs index d7d0568..2e0ac3d 100644 --- a/crates/kratad/src/control.rs +++ b/crates/kratad/src/control.rs @@ -84,6 +84,11 @@ impl ControlService for RuntimeControlService { let guest: GuestInfo = convert_guest_info( self.runtime .launch(GuestLaunchRequest { + name: if request.name.is_empty() { + None + } else { + Some(&request.name) + }, image: &oci.image, vcpus: request.vcpus, mem: request.mem, @@ -197,6 +202,7 @@ fn empty_vec_optional(value: Vec) -> Option> { fn convert_guest_info(value: kratart::GuestInfo) -> GuestInfo { GuestInfo { + name: value.name.unwrap_or_default(), id: value.uuid.to_string(), image: Some(GuestImageSpec { image: Some(Image::Oci(GuestOciImageSpec { image: value.image })), diff --git a/crates/kratad/src/lib.rs b/crates/kratad/src/lib.rs index ed20063..c133d35 100644 --- a/crates/kratad/src/lib.rs +++ b/crates/kratad/src/lib.rs @@ -4,14 +4,16 @@ use anyhow::Result; use control::RuntimeControlService; use event::{DaemonEventContext, DaemonEventGenerator}; use krata::{control::control_service_server::ControlServiceServer, dial::ControlDialAddress}; -use kratart::Runtime; -use log::info; -use tokio::{net::UnixListener, task::JoinHandle}; +use kratart::{launch::GuestLaunchRequest, Runtime}; +use log::{info, warn}; +use tab::Tab; +use tokio::{fs, net::UnixListener, task::JoinHandle}; use tokio_stream::wrappers::UnixListenerStream; use tonic::transport::{Identity, Server, ServerTlsConfig}; pub mod control; pub mod event; +pub mod tab; pub struct Daemon { store: String, @@ -32,6 +34,66 @@ impl Daemon { }) } + pub async fn load_guest_tab(&mut self) -> Result<()> { + let tab_path = PathBuf::from(format!("{}/guests.yml", self.store)); + + if !tab_path.exists() { + return Ok(()); + } + + info!("loading guest tab"); + + let tab_content = fs::read_to_string(tab_path).await?; + let tab: Tab = serde_yaml::from_str(&tab_content)?; + let running = self.runtime.list().await?; + for (name, guest) in tab.guests { + let existing = running + .iter() + .filter(|x| x.name.is_some()) + .find(|run| *run.name.as_ref().unwrap() == name); + + if let Some(existing) = existing { + info!("guest {} is already running: {}", name, existing.uuid); + continue; + } + + let request = GuestLaunchRequest { + name: Some(&name), + image: &guest.image, + vcpus: guest.cpus, + mem: guest.mem, + env: if guest.env.is_empty() { + None + } else { + Some( + guest + .env + .iter() + .map(|(key, value)| format!("{}={}", key, value)) + .collect::>(), + ) + }, + run: if guest.run.is_empty() { + None + } else { + Some(guest.run) + }, + debug: false, + }; + match self.runtime.launch(request).await { + Err(error) => { + warn!("failed to launch guest {}: {}", name, error); + } + + Ok(info) => { + info!("launched guest {}: {}", name, info.uuid); + } + } + } + info!("loaded guest tab"); + Ok(()) + } + pub async fn listen(&mut self, addr: ControlDialAddress) -> Result<()> { let control_service = RuntimeControlService::new(self.events.clone(), self.runtime.clone()); diff --git a/crates/kratad/src/tab.rs b/crates/kratad/src/tab.rs new file mode 100644 index 0000000..907b819 --- /dev/null +++ b/crates/kratad/src/tab.rs @@ -0,0 +1,20 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Tab { + #[serde(default)] + pub guests: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TabGuest { + pub image: String, + pub mem: u64, + pub cpus: u32, + #[serde(default)] + pub env: HashMap, + #[serde(default)] + pub run: Vec, +} diff --git a/crates/kratart/src/launch/mod.rs b/crates/kratart/src/launch/mod.rs index d522d3d..16f685a 100644 --- a/crates/kratart/src/launch/mod.rs +++ b/crates/kratart/src/launch/mod.rs @@ -22,6 +22,7 @@ use crate::RuntimeContext; use super::{GuestInfo, GuestState}; pub struct GuestLaunchRequest<'a> { + pub name: Option<&'a str>, pub image: &'a str, pub vcpus: u32, pub mem: u64, @@ -112,6 +113,51 @@ impl GuestLauncher { let container_mac_string = container_mac.to_string().replace('-', ":"); let gateway_mac_string = gateway_mac.to_string().replace('-', ":"); + + let mut extra_keys = vec![ + ("krata/uuid".to_string(), uuid.to_string()), + ( + "krata/loops".to_string(), + format!( + "{}:{}:none,{}:{}:{}", + &image_squashfs_loop.path, + image_squashfs_path, + &cfgblk_squashfs_loop.path, + cfgblk_squashfs_path, + cfgblk_dir_path, + ), + ), + ("krata/image".to_string(), request.image.to_string()), + ( + "krata/network/guest/ipv4".to_string(), + format!("{}/{}", guest_ipv4, ipv4_network_mask), + ), + ( + "krata/network/guest/ipv6".to_string(), + format!("{}/{}", guest_ipv6, ipv6_network_mask), + ), + ( + "krata/network/guest/mac".to_string(), + container_mac_string.clone(), + ), + ( + "krata/network/gateway/ipv4".to_string(), + format!("{}/{}", gateway_ipv4, ipv4_network_mask), + ), + ( + "krata/network/gateway/ipv6".to_string(), + format!("{}/{}", gateway_ipv6, ipv6_network_mask), + ), + ( + "krata/network/gateway/mac".to_string(), + gateway_mac_string.clone(), + ), + ]; + + if let Some(name) = request.name { + extra_keys.push(("krata/name".to_string(), name.to_string())); + } + let config = DomainConfig { backend_domid: 0, name: &name, @@ -141,49 +187,12 @@ impl GuestLauncher { }], filesystems: vec![], event_channels: vec![], - extra_keys: vec![ - ("krata/uuid".to_string(), uuid.to_string()), - ( - "krata/loops".to_string(), - format!( - "{}:{}:none,{}:{}:{}", - &image_squashfs_loop.path, - image_squashfs_path, - &cfgblk_squashfs_loop.path, - cfgblk_squashfs_path, - cfgblk_dir_path, - ), - ), - ("krata/image".to_string(), request.image.to_string()), - ( - "krata/network/guest/ipv4".to_string(), - format!("{}/{}", guest_ipv4, ipv4_network_mask), - ), - ( - "krata/network/guest/ipv6".to_string(), - format!("{}/{}", guest_ipv6, ipv6_network_mask), - ), - ( - "krata/network/guest/mac".to_string(), - container_mac_string.clone(), - ), - ( - "krata/network/gateway/ipv4".to_string(), - format!("{}/{}", gateway_ipv4, ipv4_network_mask), - ), - ( - "krata/network/gateway/ipv6".to_string(), - format!("{}/{}", gateway_ipv6, ipv6_network_mask), - ), - ( - "krata/network/gateway/mac".to_string(), - gateway_mac_string.clone(), - ), - ], + extra_keys, extra_rw_paths: vec!["krata/guest".to_string()], }; match context.xen.create(&config).await { Ok(domid) => Ok(GuestInfo { + name: request.name.map(|x| x.to_string()), uuid, domid, image: request.image.to_string(), diff --git a/crates/kratart/src/lib.rs b/crates/kratart/src/lib.rs index 3b267e8..0bd97b8 100644 --- a/crates/kratart/src/lib.rs +++ b/crates/kratart/src/lib.rs @@ -37,6 +37,7 @@ pub struct GuestState { } pub struct GuestInfo { + pub name: Option, pub uuid: Uuid, pub domid: u32, pub image: String, @@ -92,6 +93,9 @@ impl RuntimeContext { pub async fn list(&mut self) -> Result> { let mut guests: Vec = Vec::new(); for domid_candidate in self.xen.store.list("/local/domain").await? { + if domid_candidate == "0" { + continue; + } let dom_path = format!("/local/domain/{}", domid_candidate); let uuid_string = match self .xen @@ -105,6 +109,13 @@ impl RuntimeContext { let domid = u32::from_str(&domid_candidate).map_err(|_| anyhow!("failed to parse domid"))?; let uuid = Uuid::from_str(&uuid_string)?; + + let name = self + .xen + .store + .read_string(&format!("{}/krata/name", &dom_path)) + .await?; + let image = self .xen .store @@ -154,6 +165,7 @@ impl RuntimeContext { let loops = RuntimeContext::parse_loop_set(&loops); guests.push(GuestInfo { + name, uuid, domid, image, @@ -170,6 +182,13 @@ impl RuntimeContext { for guest in self.list().await? { let uuid_string = guest.uuid.to_string(); let domid_string = guest.domid.to_string(); + + if let Some(ref name) = guest.name { + if name == id { + return Ok(Some(guest)); + } + } + if uuid_string == id || domid_string == id || id == format!("krata-{}", uuid_string) { return Ok(Some(guest)); }