krata: restructure packages for cleanliness

This commit is contained in:
Alex Zenla
2024-03-30 06:17:30 +00:00
parent da9e6cac14
commit bdb91a6cb3
85 changed files with 35 additions and 1345 deletions

36
crates/runtime/Cargo.toml Normal file
View File

@ -0,0 +1,36 @@
[package]
name = "krata-runtime"
version.workspace = true
edition = "2021"
resolver = "2"
[dependencies]
advmac = { workspace = true }
anyhow = { workspace = true }
backhand = { workspace = true }
ipnetwork = { workspace = true }
krata = { path = "../krata" }
krata-oci = { path = "../oci" }
log = { workspace = true }
loopdev-3 = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
uuid = { workspace = true }
krata-xenclient = { path = "../xen/xenclient" }
krata-xenevtchn = { path = "../xen/xenevtchn" }
krata-xengnt = { path = "../xen/xengnt" }
krata-xenstore = { path = "../xen/xenstore" }
[lib]
name = "kratart"
[dev-dependencies]
env_logger = { workspace = true }
[[example]]
name = "kratart-squashify"
path = "examples/squashify.rs"
[[example]]
name = "kratart-channel"
path = "examples/channel.rs"

View File

@ -0,0 +1,23 @@
use anyhow::Result;
use env_logger::Env;
use kratart::channel::ChannelService;
#[tokio::main]
async fn main() -> Result<()> {
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
let (service, mut receiver) = ChannelService::new("krata-channel".to_string()).await?;
let task = service.launch().await?;
loop {
let Some((id, data)) = receiver.recv().await else {
break;
};
println!("domain {} = {:?}", id, data);
}
task.abort();
Ok(())
}

View File

@ -0,0 +1,29 @@
use std::{env::args, path::PathBuf};
use anyhow::Result;
use env_logger::Env;
use krataoci::{cache::ImageCache, compiler::ImageCompiler, name::ImageName};
use tokio::fs;
#[tokio::main]
async fn main() -> Result<()> {
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
let image = ImageName::parse(&args().nth(1).unwrap())?;
let seed = args().nth(2).map(PathBuf::from);
let cache_dir = PathBuf::from("krata-cache");
if !cache_dir.exists() {
fs::create_dir(&cache_dir).await?;
}
let cache = ImageCache::new(&cache_dir)?;
let compiler = ImageCompiler::new(&cache, seed)?;
let info = compiler.compile(&image).await?;
println!(
"generated squashfs of {} to {}",
image,
info.image_squashfs.to_string_lossy()
);
Ok(())
}

View File

@ -0,0 +1,33 @@
use anyhow::{anyhow, Result};
use loopdev::{LoopControl, LoopDevice};
use xenclient::BlockDeviceRef;
pub struct AutoLoop {
control: LoopControl,
}
impl AutoLoop {
pub fn new(control: LoopControl) -> AutoLoop {
AutoLoop { control }
}
pub fn loopify(&self, file: &str) -> Result<BlockDeviceRef> {
let device = self.control.next_free()?;
device.with().read_only(true).attach(file)?;
let path = device
.path()
.ok_or(anyhow!("unable to get loop device path"))?
.to_str()
.ok_or(anyhow!("unable to convert loop device path to string",))?
.to_string();
let major = device.major()?;
let minor = device.minor()?;
Ok(BlockDeviceRef { path, major, minor })
}
pub fn unloop(&self, device: &str) -> Result<()> {
let device = LoopDevice::open(device)?;
device.detach()?;
Ok(())
}
}

View File

@ -0,0 +1,71 @@
use anyhow::Result;
use backhand::{FilesystemWriter, NodeHeader};
use krata::launchcfg::LaunchInfo;
use krataoci::compiler::ImageInfo;
use log::trace;
use std::fs;
use std::fs::File;
use std::path::PathBuf;
use uuid::Uuid;
pub struct ConfigBlock<'a> {
pub image_info: &'a ImageInfo,
pub file: PathBuf,
pub dir: PathBuf,
}
impl ConfigBlock<'_> {
pub fn new<'a>(uuid: &Uuid, image_info: &'a ImageInfo) -> Result<ConfigBlock<'a>> {
let mut dir = std::env::temp_dir().clone();
dir.push(format!("krata-cfg-{}", uuid));
fs::create_dir_all(&dir)?;
let mut file = dir.clone();
file.push("config.squashfs");
Ok(ConfigBlock {
image_info,
file,
dir,
})
}
pub fn build(&self, launch_config: &LaunchInfo) -> Result<()> {
trace!("build launch_config={:?}", launch_config);
let manifest = self.image_info.config.to_string()?;
let launch = serde_json::to_string(launch_config)?;
let mut writer = FilesystemWriter::default();
writer.push_dir(
"/image",
NodeHeader {
permissions: 384,
uid: 0,
gid: 0,
mtime: 0,
},
)?;
writer.push_file(
manifest.as_bytes(),
"/image/config.json",
NodeHeader {
permissions: 384,
uid: 0,
gid: 0,
mtime: 0,
},
)?;
writer.push_file(
launch.as_bytes(),
"/launch.json",
NodeHeader {
permissions: 384,
uid: 0,
gid: 0,
mtime: 0,
},
)?;
let mut file = File::create(&self.file)?;
trace!("build write sqaushfs");
writer.write(&mut file)?;
trace!("build complete");
Ok(())
}
}

View File

@ -0,0 +1,501 @@
use std::{
collections::HashMap,
sync::atomic::{fence, Ordering},
time::Duration,
};
use anyhow::{anyhow, Result};
use log::{debug, error};
use tokio::{
select,
sync::{
broadcast,
mpsc::{channel, Receiver, Sender},
},
task::JoinHandle,
time::sleep,
};
use xenevtchn::EventChannel;
use xengnt::{sys::GrantRef, GrantTab, MappedMemory};
use xenstore::{XsdClient, XsdInterface};
const SINGLE_CHANNEL_QUEUE_LEN: usize = 100;
const GROUPED_CHANNEL_QUEUE_LEN: usize = 1000;
#[repr(C)]
struct XenConsoleInterface {
input: [u8; XenConsoleInterface::INPUT_SIZE],
output: [u8; XenConsoleInterface::OUTPUT_SIZE],
in_cons: u32,
in_prod: u32,
out_cons: u32,
out_prod: u32,
}
unsafe impl Send for XenConsoleInterface {}
impl XenConsoleInterface {
const INPUT_SIZE: usize = 1024;
const OUTPUT_SIZE: usize = 2048;
}
pub struct ChannelService {
typ: String,
backends: HashMap<u32, ChannelBackend>,
evtchn: EventChannel,
store: XsdClient,
gnttab: GrantTab,
input_receiver: Receiver<(u32, Vec<u8>)>,
pub input_sender: Sender<(u32, Vec<u8>)>,
output_sender: Sender<(u32, Vec<u8>)>,
}
impl ChannelService {
pub async fn new(typ: String) -> Result<(ChannelService, Receiver<(u32, Vec<u8>)>)> {
let (input_sender, input_receiver) = channel(GROUPED_CHANNEL_QUEUE_LEN);
let (output_sender, output_receiver) = channel(GROUPED_CHANNEL_QUEUE_LEN);
Ok((
ChannelService {
typ,
backends: HashMap::new(),
evtchn: EventChannel::open().await?,
store: XsdClient::open().await?,
gnttab: GrantTab::open()?,
input_sender,
input_receiver,
output_sender,
},
output_receiver,
))
}
pub async fn launch(mut self) -> Result<JoinHandle<()>> {
Ok(tokio::task::spawn(async move {
if let Err(error) = self.process().await {
error!("channel processor failed: {}", error);
}
}))
}
async fn process(&mut self) -> Result<()> {
self.scan_all_backends().await?;
let mut watch_handle = self
.store
.create_watch("/local/domain/0/backend/console")
.await?;
self.store.bind_watch(&watch_handle).await?;
loop {
select! {
x = watch_handle.receiver.recv() => match x {
Some(_) => {
self.scan_all_backends().await?;
}
None => {
break;
}
},
x = self.input_receiver.recv() => match x {
Some((domid, data)) => {
if let Some(backend) = self.backends.get_mut(&domid) {
let _ = backend.sender.try_send(data);
}
},
None => {
break;
}
}
}
}
Ok(())
}
pub async fn send(&mut self, domid: u32, message: Vec<u8>) -> Result<()> {
if let Some(backend) = self.backends.get(&domid) {
backend.sender.send(message).await?;
}
Ok(())
}
async fn ensure_backend_exists(&mut self, domid: u32, id: u32, path: String) -> Result<()> {
if self.backends.contains_key(&domid) {
return Ok(());
}
let Some(frontend_path) = self.store.read_string(format!("{}/frontend", path)).await?
else {
return Ok(());
};
let Some(typ) = self
.store
.read_string(format!("{}/type", frontend_path))
.await?
else {
return Ok(());
};
if typ != self.typ {
return Ok(());
}
let backend = ChannelBackend::new(
path.clone(),
frontend_path.clone(),
domid,
id,
self.store.clone(),
self.evtchn.clone(),
self.gnttab.clone(),
self.output_sender.clone(),
)
.await?;
self.backends.insert(domid, backend);
Ok(())
}
async fn scan_all_backends(&mut self) -> Result<()> {
let domains = self.store.list("/local/domain/0/backend/console").await?;
let mut seen: Vec<u32> = Vec::new();
for domid_string in &domains {
let domid = domid_string.parse::<u32>()?;
let domid_path = format!("/local/domain/0/backend/console/{}", domid);
for id_string in self.store.list(&domid_path).await? {
let id = id_string.parse::<u32>()?;
let console_path = format!(
"/local/domain/0/backend/console/{}/{}",
domid_string, id_string
);
self.ensure_backend_exists(domid, id, console_path).await?;
seen.push(domid);
}
}
let mut gone: Vec<u32> = Vec::new();
for backend in self.backends.keys() {
if !seen.contains(backend) {
gone.push(*backend);
}
}
for item in gone {
if let Some(backend) = self.backends.remove(&item) {
drop(backend);
}
}
Ok(())
}
}
pub struct ChannelBackend {
pub domid: u32,
pub id: u32,
pub sender: Sender<Vec<u8>>,
task: JoinHandle<()>,
}
impl Drop for ChannelBackend {
fn drop(&mut self) {
self.task.abort();
debug!(
"destroyed channel backend for domain {} channel {}",
self.domid, self.id
);
}
}
impl ChannelBackend {
#[allow(clippy::too_many_arguments)]
pub async fn new(
backend: String,
frontend: String,
domid: u32,
id: u32,
store: XsdClient,
evtchn: EventChannel,
gnttab: GrantTab,
output_sender: Sender<(u32, Vec<u8>)>,
) -> Result<ChannelBackend> {
let processor = KrataChannelBackendProcessor {
backend,
frontend,
domid,
id,
store,
evtchn,
gnttab,
};
let (input_sender, input_receiver) = channel(SINGLE_CHANNEL_QUEUE_LEN);
let task = processor.launch(output_sender, input_receiver).await?;
Ok(ChannelBackend {
domid,
id,
task,
sender: input_sender,
})
}
}
#[derive(Clone)]
pub struct KrataChannelBackendProcessor {
backend: String,
frontend: String,
id: u32,
domid: u32,
store: XsdClient,
evtchn: EventChannel,
gnttab: GrantTab,
}
impl KrataChannelBackendProcessor {
async fn init(&self) -> Result<()> {
self.store
.write_string(format!("{}/state", self.backend), "3")
.await?;
debug!(
"created channel backend for domain {} channel {}",
self.domid, self.id
);
Ok(())
}
async fn on_frontend_state_change(&self) -> Result<bool> {
let state = self
.store
.read_string(format!("{}/state", self.backend))
.await?
.unwrap_or("0".to_string())
.parse::<u32>()?;
if state == 3 {
return Ok(true);
}
Ok(false)
}
async fn on_self_state_change(&self) -> Result<bool> {
let state = self
.store
.read_string(format!("{}/state", self.backend))
.await?
.unwrap_or("0".to_string())
.parse::<u32>()?;
if state == 5 {
return Ok(true);
}
Ok(false)
}
async fn launch(
&self,
output_sender: Sender<(u32, Vec<u8>)>,
input_receiver: Receiver<Vec<u8>>,
) -> Result<JoinHandle<()>> {
let owned = self.clone();
Ok(tokio::task::spawn(async move {
if let Err(error) = owned.processor(output_sender, input_receiver).await {
error!("failed to process krata channel: {}", error);
}
let _ = owned
.store
.write_string(format!("{}/state", owned.backend), "6")
.await;
}))
}
async fn processor(
&self,
sender: Sender<(u32, Vec<u8>)>,
mut receiver: Receiver<Vec<u8>>,
) -> Result<()> {
self.init().await?;
let mut frontend_state_change = self
.store
.create_watch(format!("{}/state", self.frontend))
.await?;
self.store.bind_watch(&frontend_state_change).await?;
let (ring_ref, port) = loop {
match frontend_state_change.receiver.recv().await {
Some(_) => {
if self.on_frontend_state_change().await? {
let mut tries = 0;
let (ring_ref, port) = loop {
let ring_ref = self
.store
.read_string(format!("{}/ring-ref", self.frontend))
.await?;
let port = self
.store
.read_string(format!("{}/port", self.frontend))
.await?;
if (ring_ref.is_none() || port.is_none()) && tries < 10 {
tries += 1;
self.store
.write_string(format!("{}/state", self.backend), "4")
.await?;
sleep(Duration::from_millis(250)).await;
continue;
}
break (ring_ref, port);
};
if ring_ref.is_none() || port.is_none() {
return Err(anyhow!("frontend did not give ring-ref and port"));
}
let Ok(ring_ref) = ring_ref.unwrap().parse::<u64>() else {
return Err(anyhow!("frontend gave invalid ring-ref"));
};
let Ok(port) = port.unwrap().parse::<u32>() else {
return Err(anyhow!("frontend gave invalid port"));
};
break (ring_ref, port);
}
}
None => {
return Ok(());
}
}
};
self.store
.write_string(format!("{}/state", self.backend), "4")
.await?;
let memory = self.gnttab.map_grant_refs(
vec![GrantRef {
domid: self.domid,
reference: ring_ref as u32,
}],
true,
true,
)?;
let mut channel = self.evtchn.bind(self.domid, port).await?;
unsafe {
let buffer = self.read_output_buffer(channel.local_port, &memory).await?;
if !buffer.is_empty() {
sender.send((self.domid, buffer)).await?;
}
};
let mut self_state_change = self
.store
.create_watch(format!("{}/state", self.backend))
.await?;
self.store.bind_watch(&self_state_change).await?;
loop {
select! {
x = self_state_change.receiver.recv() => match x {
Some(_) => {
match self.on_self_state_change().await {
Err(error) => {
error!("failed to process state change for domain {} channel {}: {}", self.domid, self.id, error);
},
Ok(stop) => {
if stop {
break;
}
}
}
},
None => {
break;
}
},
x = receiver.recv() => match x {
Some(data) => {
let mut index = 0;
loop {
if index >= data.len() {
break;
}
let interface = memory.ptr() as *mut XenConsoleInterface;
let cons = unsafe { (*interface).in_cons };
let mut prod = unsafe { (*interface).in_prod };
fence(Ordering::Release);
let space = (prod - cons) as usize;
if space > XenConsoleInterface::INPUT_SIZE {
error!("channel for domid {} has an invalid input space of {}", self.domid, space);
}
let free = XenConsoleInterface::INPUT_SIZE.wrapping_sub(space);
let want = data.len().min(free);
let buffer = &data[index..want];
for b in buffer {
unsafe { (*interface).input[prod as usize & (XenConsoleInterface::INPUT_SIZE - 1)] = *b; };
prod = prod.wrapping_add(1);
}
fence(Ordering::Release);
unsafe { (*interface).in_prod = prod; };
self.evtchn.notify(channel.local_port).await?;
index += want;
}
},
None => {
break;
}
},
x = channel.receiver.recv() => match x {
Ok(_) => {
unsafe {
let buffer = self.read_output_buffer(channel.local_port, &memory).await?;
if !buffer.is_empty() {
sender.send((self.domid, buffer)).await?;
}
};
channel.unmask_sender.send(channel.local_port).await?;
},
Err(error) => {
match error {
broadcast::error::RecvError::Closed => {
break;
},
error => {
return Err(anyhow!("failed to receive event notification: {}", error));
}
}
}
}
};
}
Ok(())
}
async unsafe fn read_output_buffer<'a>(
&self,
local_port: u32,
memory: &MappedMemory<'a>,
) -> Result<Vec<u8>> {
let interface = memory.ptr() as *mut XenConsoleInterface;
let mut cons = (*interface).out_cons;
let prod = (*interface).out_prod;
fence(Ordering::Release);
let size = prod.wrapping_sub(cons);
let mut data: Vec<u8> = Vec::new();
if size == 0 || size as usize > XenConsoleInterface::OUTPUT_SIZE {
return Ok(data);
}
loop {
if cons == prod {
break;
}
data.push((*interface).output[cons as usize & (XenConsoleInterface::OUTPUT_SIZE - 1)]);
cons = cons.wrapping_add(1);
}
fence(Ordering::AcqRel);
(*interface).out_cons = cons;
self.evtchn.notify(local_port).await?;
Ok(data)
}
}

View File

@ -0,0 +1,18 @@
use anyhow::Result;
use tokio::fs::File;
pub struct XenConsole {
pub read_handle: File,
pub write_handle: File,
}
impl XenConsole {
pub async fn new(tty: &str) -> Result<XenConsole> {
let read_handle = File::options().read(true).write(false).open(tty).await?;
let write_handle = File::options().read(false).write(true).open(tty).await?;
Ok(XenConsole {
read_handle,
write_handle,
})
}
}

View File

@ -0,0 +1,273 @@
use std::collections::HashMap;
use std::net::{IpAddr, Ipv6Addr};
use std::{fs, net::Ipv4Addr, str::FromStr};
use advmac::MacAddr6;
use anyhow::{anyhow, Result};
use ipnetwork::{IpNetwork, Ipv4Network};
use krata::launchcfg::{
LaunchInfo, LaunchNetwork, LaunchNetworkIpv4, LaunchNetworkIpv6, LaunchNetworkResolver,
};
use uuid::Uuid;
use xenclient::{DomainChannel, DomainConfig, DomainDisk, DomainNetworkInterface};
use xenstore::XsdInterface;
use crate::cfgblk::ConfigBlock;
use crate::RuntimeContext;
use krataoci::{
cache::ImageCache,
compiler::{ImageCompiler, ImageInfo},
name::ImageName,
};
use super::{GuestInfo, GuestState};
pub struct GuestLaunchRequest<'a> {
pub uuid: Option<Uuid>,
pub name: Option<&'a str>,
pub image: &'a str,
pub vcpus: u32,
pub mem: u64,
pub env: HashMap<String, String>,
pub run: Option<Vec<String>>,
pub debug: bool,
}
pub struct GuestLauncher {}
impl GuestLauncher {
pub fn new() -> Result<Self> {
Ok(Self {})
}
pub async fn launch<'r>(
&mut self,
context: &mut RuntimeContext,
request: GuestLaunchRequest<'r>,
) -> Result<GuestInfo> {
let uuid = request.uuid.unwrap_or_else(Uuid::new_v4);
let xen_name = format!("krata-{uuid}");
let image_info = self.compile(request.image, &context.image_cache).await?;
let mut gateway_mac = MacAddr6::random();
gateway_mac.set_local(true);
gateway_mac.set_multicast(false);
let mut container_mac = MacAddr6::random();
container_mac.set_local(true);
container_mac.set_multicast(false);
let guest_ipv4 = self.allocate_ipv4(context).await?;
let guest_ipv6 = container_mac.to_link_local_ipv6();
let gateway_ipv4 = "10.75.70.1";
let gateway_ipv6 = "fe80::1";
let ipv4_network_mask: u32 = 16;
let ipv6_network_mask: u32 = 10;
let launch_config = LaunchInfo {
network: Some(LaunchNetwork {
link: "eth0".to_string(),
ipv4: LaunchNetworkIpv4 {
address: format!("{}/{}", guest_ipv4, ipv4_network_mask),
gateway: gateway_ipv4.to_string(),
},
ipv6: LaunchNetworkIpv6 {
address: format!("{}/{}", guest_ipv6, ipv6_network_mask),
gateway: gateway_ipv6.to_string(),
},
resolver: LaunchNetworkResolver {
nameservers: vec![
"1.1.1.1".to_string(),
"1.0.0.1".to_string(),
"2606:4700:4700::1111".to_string(),
"2606:4700:4700::1001".to_string(),
],
},
}),
env: request.env,
run: request.run,
};
let cfgblk = ConfigBlock::new(&uuid, &image_info)?;
cfgblk.build(&launch_config)?;
let image_squashfs_path = image_info
.image_squashfs
.to_str()
.ok_or_else(|| anyhow!("failed to convert image squashfs path to string"))?;
let cfgblk_dir_path = cfgblk
.dir
.to_str()
.ok_or_else(|| anyhow!("failed to convert cfgblk directory path to string"))?;
let cfgblk_squashfs_path = cfgblk
.file
.to_str()
.ok_or_else(|| anyhow!("failed to convert cfgblk squashfs path to string"))?;
let image_squashfs_loop = context.autoloop.loopify(image_squashfs_path)?;
let cfgblk_squashfs_loop = context.autoloop.loopify(cfgblk_squashfs_path)?;
let cmdline_options = [
if request.debug { "debug" } else { "quiet" },
"elevator=noop",
];
let cmdline = cmdline_options.join(" ");
let guest_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(),
guest_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: &xen_name,
max_vcpus: request.vcpus,
mem_mb: request.mem,
kernel_path: &context.kernel,
initrd_path: &context.initrd,
cmdline: &cmdline,
disks: vec![
DomainDisk {
vdev: "xvda",
block: &image_squashfs_loop,
writable: false,
},
DomainDisk {
vdev: "xvdb",
block: &cfgblk_squashfs_loop,
writable: false,
},
],
channels: vec![DomainChannel {
typ: "krata-channel".to_string(),
initialized: false,
}],
vifs: vec![DomainNetworkInterface {
mac: &guest_mac_string,
mtu: 1500,
bridge: None,
script: None,
}],
filesystems: vec![],
event_channels: vec![],
extra_keys,
extra_rw_paths: vec!["krata/guest".to_string()],
};
match context.xen.create(&config).await {
Ok(created) => Ok(GuestInfo {
name: request.name.map(|x| x.to_string()),
uuid,
domid: created.domid,
image: request.image.to_string(),
loops: vec![],
guest_ipv4: Some(IpNetwork::new(
IpAddr::V4(guest_ipv4),
ipv4_network_mask as u8,
)?),
guest_ipv6: Some(IpNetwork::new(
IpAddr::V6(guest_ipv6),
ipv6_network_mask as u8,
)?),
guest_mac: Some(guest_mac_string.clone()),
gateway_ipv4: Some(IpNetwork::new(
IpAddr::V4(Ipv4Addr::from_str(gateway_ipv4)?),
ipv4_network_mask as u8,
)?),
gateway_ipv6: Some(IpNetwork::new(
IpAddr::V6(Ipv6Addr::from_str(gateway_ipv6)?),
ipv4_network_mask as u8,
)?),
gateway_mac: Some(gateway_mac_string.clone()),
state: GuestState { exit_code: None },
}),
Err(error) => {
let _ = context.autoloop.unloop(&image_squashfs_loop.path);
let _ = context.autoloop.unloop(&cfgblk_squashfs_loop.path);
let _ = fs::remove_dir(&cfgblk.dir);
Err(error.into())
}
}
}
async fn compile(&self, image: &str, image_cache: &ImageCache) -> Result<ImageInfo> {
let image = ImageName::parse(image)?;
let compiler = ImageCompiler::new(image_cache, None)?;
compiler.compile(&image).await
}
async fn allocate_ipv4(&mut self, context: &mut RuntimeContext) -> Result<Ipv4Addr> {
let network = Ipv4Network::new(Ipv4Addr::new(10, 75, 80, 0), 24)?;
let mut used: Vec<Ipv4Addr> = vec![];
for domid_candidate in context.xen.store.list("/local/domain").await? {
let dom_path = format!("/local/domain/{}", domid_candidate);
let ip_path = format!("{}/krata/network/guest/ipv4", dom_path);
let existing_ip = context.xen.store.read_string(&ip_path).await?;
if let Some(existing_ip) = existing_ip {
let ipv4_network = Ipv4Network::from_str(&existing_ip)?;
used.push(ipv4_network.ip());
}
}
let mut found: Option<Ipv4Addr> = None;
for ip in network.iter() {
let last = ip.octets()[3];
if last == 0 || last == 255 {
continue;
}
if !used.contains(&ip) {
found = Some(ip);
break;
}
}
if found.is_none() {
return Err(anyhow!(
"unable to find ipv4 to allocate to container, ipv4 addresses are exhausted"
));
}
Ok(found.unwrap())
}
}

347
crates/runtime/src/lib.rs Normal file
View File

@ -0,0 +1,347 @@
use std::{
fs,
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
};
use anyhow::{anyhow, Result};
use ipnetwork::IpNetwork;
use loopdev::LoopControl;
use tokio::sync::Mutex;
use uuid::Uuid;
use xenclient::XenClient;
use xenstore::{XsdClient, XsdInterface};
use self::{
autoloop::AutoLoop,
console::XenConsole,
launch::{GuestLaunchRequest, GuestLauncher},
};
use krataoci::cache::ImageCache;
pub mod autoloop;
pub mod cfgblk;
pub mod channel;
pub mod console;
pub mod launch;
pub struct GuestLoopInfo {
pub device: String,
pub file: String,
pub delete: Option<String>,
}
pub struct GuestState {
pub exit_code: Option<i32>,
}
pub struct GuestInfo {
pub name: Option<String>,
pub uuid: Uuid,
pub domid: u32,
pub image: String,
pub loops: Vec<GuestLoopInfo>,
pub guest_ipv4: Option<IpNetwork>,
pub guest_ipv6: Option<IpNetwork>,
pub guest_mac: Option<String>,
pub gateway_ipv4: Option<IpNetwork>,
pub gateway_ipv6: Option<IpNetwork>,
pub gateway_mac: Option<String>,
pub state: GuestState,
}
pub struct RuntimeContext {
pub image_cache: ImageCache,
pub autoloop: AutoLoop,
pub xen: XenClient,
pub kernel: String,
pub initrd: String,
}
impl RuntimeContext {
pub async fn new(store: String) -> Result<Self> {
let mut image_cache_path = PathBuf::from(&store);
image_cache_path.push("cache");
fs::create_dir_all(&image_cache_path)?;
let xen = XenClient::open(0).await?;
image_cache_path.push("image");
fs::create_dir_all(&image_cache_path)?;
let image_cache = ImageCache::new(&image_cache_path)?;
let kernel = RuntimeContext::detect_guest_file(&store, "kernel")?;
let initrd = RuntimeContext::detect_guest_file(&store, "initrd")?;
Ok(RuntimeContext {
image_cache,
autoloop: AutoLoop::new(LoopControl::open()?),
xen,
kernel,
initrd,
})
}
fn detect_guest_file(store: &str, name: &str) -> Result<String> {
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(&mut self) -> Result<Vec<GuestInfo>> {
let mut guests: Vec<GuestInfo> = 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
.store
.read_string(&format!("{}/krata/uuid", &dom_path))
.await?
{
None => continue,
Some(value) => value,
};
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
.read_string(&format!("{}/krata/image", &dom_path))
.await?
.unwrap_or("unknown".to_string());
let loops = self
.xen
.store
.read_string(&format!("{}/krata/loops", &dom_path))
.await?;
let guest_ipv4 = self
.xen
.store
.read_string(&format!("{}/krata/network/guest/ipv4", &dom_path))
.await?;
let guest_ipv6 = self
.xen
.store
.read_string(&format!("{}/krata/network/guest/ipv6", &dom_path))
.await?;
let guest_mac = self
.xen
.store
.read_string(&format!("{}/krata/network/guest/mac", &dom_path))
.await?;
let gateway_ipv4 = self
.xen
.store
.read_string(&format!("{}/krata/network/gateway/ipv4", &dom_path))
.await?;
let gateway_ipv6 = self
.xen
.store
.read_string(&format!("{}/krata/network/gateway/ipv6", &dom_path))
.await?;
let gateway_mac = self
.xen
.store
.read_string(&format!("{}/krata/network/gateway/mac", &dom_path))
.await?;
let guest_ipv4 = if let Some(guest_ipv4) = guest_ipv4 {
IpNetwork::from_str(&guest_ipv4).ok()
} else {
None
};
let guest_ipv6 = if let Some(guest_ipv6) = guest_ipv6 {
IpNetwork::from_str(&guest_ipv6).ok()
} else {
None
};
let gateway_ipv4 = if let Some(gateway_ipv4) = gateway_ipv4 {
IpNetwork::from_str(&gateway_ipv4).ok()
} else {
None
};
let gateway_ipv6 = if let Some(gateway_ipv6) = gateway_ipv6 {
IpNetwork::from_str(&gateway_ipv6).ok()
} else {
None
};
let exit_code = self
.xen
.store
.read_string(&format!("{}/krata/guest/exit-code", &dom_path))
.await?;
let exit_code: Option<i32> = match exit_code {
Some(code) => code.parse().ok(),
None => None,
};
let state = GuestState { exit_code };
let loops = RuntimeContext::parse_loop_set(&loops);
guests.push(GuestInfo {
name,
uuid,
domid,
image,
loops,
guest_ipv4,
guest_ipv6,
guest_mac,
gateway_ipv4,
gateway_ipv6,
gateway_mac,
state,
});
}
Ok(guests)
}
pub async fn resolve(&mut self, uuid: Uuid) -> Result<Option<GuestInfo>> {
for guest in self.list().await? {
if guest.uuid == uuid {
return Ok(Some(guest));
}
}
Ok(None)
}
fn parse_loop_set(input: &Option<String>) -> Vec<GuestLoopInfo> {
let Some(input) = input else {
return Vec::new();
};
let sets = input
.split(',')
.map(|x| x.to_string())
.map(|x| x.split(':').map(|v| v.to_string()).collect::<Vec<String>>())
.map(|x| (x[0].clone(), x[1].clone(), x[2].clone()))
.collect::<Vec<(String, String, String)>>();
sets.iter()
.map(|(device, file, delete)| GuestLoopInfo {
device: device.clone(),
file: file.clone(),
delete: if delete == "none" {
None
} else {
Some(delete.clone())
},
})
.collect::<Vec<GuestLoopInfo>>()
}
}
#[derive(Clone)]
pub struct Runtime {
store: Arc<String>,
context: Arc<Mutex<RuntimeContext>>,
}
impl Runtime {
pub async fn new(store: String) -> Result<Self> {
let context = RuntimeContext::new(store.clone()).await?;
Ok(Self {
store: Arc::new(store),
context: Arc::new(Mutex::new(context)),
})
}
pub async fn launch<'a>(&self, request: GuestLaunchRequest<'a>) -> Result<GuestInfo> {
let mut context = self.context.lock().await;
let mut launcher = GuestLauncher::new()?;
launcher.launch(&mut context, request).await
}
pub async fn destroy(&self, uuid: Uuid) -> Result<Uuid> {
let mut context = self.context.lock().await;
let info = context
.resolve(uuid)
.await?
.ok_or_else(|| anyhow!("unable to resolve guest: {}", uuid))?;
let domid = info.domid;
let mut store = XsdClient::open().await?;
let dom_path = store.get_domain_path(domid).await?;
let uuid = match store
.read_string(format!("{}/krata/uuid", dom_path).as_str())
.await?
{
None => {
return Err(anyhow!(
"domain {} was not found or not created by krata",
domid
))
}
Some(value) => value,
};
if uuid.is_empty() {
return Err(anyhow!("unable to find krata uuid based on the domain",));
}
let uuid = Uuid::parse_str(&uuid)?;
let loops = store
.read_string(format!("{}/krata/loops", dom_path).as_str())
.await?;
let loops = RuntimeContext::parse_loop_set(&loops);
context.xen.destroy(domid).await?;
for info in &loops {
context.autoloop.unloop(&info.device)?;
match &info.delete {
None => {}
Some(delete) => {
let delete_path = PathBuf::from(delete);
if delete_path.is_file() || delete_path.is_symlink() {
fs::remove_file(&delete_path)?;
} else if delete_path.is_dir() {
fs::remove_dir_all(&delete_path)?;
}
}
}
}
Ok(uuid)
}
pub async fn console(&self, uuid: Uuid) -> Result<XenConsole> {
let mut context = self.context.lock().await;
let info = context
.resolve(uuid)
.await?
.ok_or_else(|| anyhow!("unable to resolve guest: {}", uuid))?;
let domid = info.domid;
let tty = context.xen.get_console_path(domid).await?;
XenConsole::new(&tty).await
}
pub async fn list(&self) -> Result<Vec<GuestInfo>> {
let mut context = self.context.lock().await;
context.list().await
}
pub async fn dupe(&self) -> Result<Runtime> {
Runtime::new((*self.store).clone()).await
}
}
fn path_as_string(path: &Path) -> Result<String> {
path.to_str()
.ok_or_else(|| anyhow!("unable to convert path to string"))
.map(|x| x.to_string())
}