mirror of
https://github.com/edera-dev/krata.git
synced 2025-08-03 13:11:31 +00:00
krata xen control-plane
This commit is contained in:
@ -1,25 +0,0 @@
|
||||
[package]
|
||||
name = "krata-buildtools"
|
||||
description = "Build tools for krata."
|
||||
license.workspace = true
|
||||
version.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
oci-spec = { workspace = true }
|
||||
scopeguard = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
krata-oci = { path = "../oci", version = "^0.0.21" }
|
||||
krata-tokio-tar = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
||||
[[bin]]
|
||||
name = "build-fetch-kernel"
|
||||
path = "bin/fetch_kernel.rs"
|
@ -1,121 +0,0 @@
|
||||
use std::{
|
||||
env::{self, args},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use env_logger::Env;
|
||||
use krataoci::{
|
||||
name::ImageName,
|
||||
packer::{service::OciPackerService, OciPackedFormat},
|
||||
progress::OciProgressContext,
|
||||
registry::OciPlatform,
|
||||
};
|
||||
use oci_spec::image::{Arch, Os};
|
||||
use tokio::{
|
||||
fs::{self, File},
|
||||
io::BufReader,
|
||||
};
|
||||
use tokio_stream::StreamExt;
|
||||
use tokio_tar::Archive;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
env_logger::Builder::from_env(Env::default().default_filter_or("warn")).init();
|
||||
fs::create_dir_all("target/kernel").await?;
|
||||
|
||||
let arch = env::var("TARGET_ARCH").map_err(|_| anyhow!("missing TARGET_ARCH env var"))?;
|
||||
println!("kernel architecture: {}", arch);
|
||||
let platform = OciPlatform::new(
|
||||
Os::Linux,
|
||||
match arch.as_str() {
|
||||
"x86_64" => Arch::Amd64,
|
||||
"aarch64" => Arch::ARM64,
|
||||
_ => {
|
||||
return Err(anyhow!("unknown architecture '{}'", arch));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let image = ImageName::parse(&args().nth(1).unwrap())?;
|
||||
let mut cache_dir = env::temp_dir().clone();
|
||||
cache_dir.push(format!("krata-cache-{}", Uuid::new_v4()));
|
||||
fs::create_dir_all(&cache_dir).await?;
|
||||
|
||||
let _delete_cache_dir = scopeguard::guard(cache_dir.clone(), |dir| {
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
});
|
||||
|
||||
let (context, _) = OciProgressContext::create();
|
||||
let service = OciPackerService::new(None, &cache_dir, platform).await?;
|
||||
let packed = service
|
||||
.request(image.clone(), OciPackedFormat::Tar, false, true, context)
|
||||
.await?;
|
||||
let annotations = packed
|
||||
.manifest
|
||||
.item()
|
||||
.annotations()
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
let Some(format) = annotations.get("dev.edera.kernel.format") else {
|
||||
return Err(anyhow!(
|
||||
"image manifest missing 'dev.edera.kernel.format' annotation"
|
||||
));
|
||||
};
|
||||
let Some(version) = annotations.get("dev.edera.kernel.version") else {
|
||||
return Err(anyhow!(
|
||||
"image manifest missing 'dev.edera.kernel.version' annotation"
|
||||
));
|
||||
};
|
||||
let Some(flavor) = annotations.get("dev.edera.kernel.flavor") else {
|
||||
return Err(anyhow!(
|
||||
"image manifest missing 'dev.edera.kernel.flavor' annotation"
|
||||
));
|
||||
};
|
||||
|
||||
if format != "1" {
|
||||
return Err(anyhow!("kernel format version '{}' is unknown", format));
|
||||
}
|
||||
|
||||
let file = BufReader::new(File::open(packed.path).await?);
|
||||
let mut archive = Archive::new(file);
|
||||
let mut entries = archive.entries()?;
|
||||
|
||||
let kernel_image_tar_path = PathBuf::from("kernel/image");
|
||||
let kernel_addons_tar_path = PathBuf::from("kernel/addons.squashfs");
|
||||
let kernel_image_out_path = PathBuf::from(format!("target/kernel/kernel-{}", arch));
|
||||
let kernel_addons_out_path = PathBuf::from(format!("target/kernel/addons-{}.squashfs", arch));
|
||||
|
||||
if kernel_image_out_path.exists() {
|
||||
fs::remove_file(&kernel_image_out_path).await?;
|
||||
}
|
||||
|
||||
if kernel_addons_out_path.exists() {
|
||||
fs::remove_file(&kernel_addons_out_path).await?;
|
||||
}
|
||||
|
||||
while let Some(entry) = entries.next().await {
|
||||
let mut entry = entry?;
|
||||
let path = entry.path()?.to_path_buf();
|
||||
|
||||
if !entry.header().entry_type().is_file() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if path == kernel_image_tar_path {
|
||||
entry.unpack(&kernel_image_out_path).await?;
|
||||
} else if path == kernel_addons_tar_path {
|
||||
entry.unpack(&kernel_addons_out_path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
if !kernel_image_out_path.exists() {
|
||||
return Err(anyhow!("image did not contain a file named /kernel/image"));
|
||||
}
|
||||
|
||||
println!("kernel version: v{}", version);
|
||||
println!("kernel flavor: {}", flavor);
|
||||
|
||||
Ok(())
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
[package]
|
||||
name = "krata-ctl"
|
||||
description = "Command-line tool to control the krata isolation engine"
|
||||
license.workspace = true
|
||||
version.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
async-stream = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
comfy-table = { workspace = true }
|
||||
crossterm = { workspace = true, features = ["event-stream"] }
|
||||
ctrlc = { workspace = true, features = ["termination"] }
|
||||
env_logger = { workspace = true }
|
||||
fancy-duration = { workspace = true }
|
||||
human_bytes = { workspace = true }
|
||||
indicatif = { workspace = true }
|
||||
krata = { path = "../krata", version = "^0.0.21" }
|
||||
log = { workspace = true }
|
||||
prost-reflect = { workspace = true, features = ["serde"] }
|
||||
prost-types = { workspace = true }
|
||||
ratatui = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = { workspace = true }
|
||||
termtree = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
tonic = { workspace = true }
|
||||
tower = { workspace = true }
|
||||
|
||||
[lib]
|
||||
name = "kratactl"
|
||||
|
||||
[[bin]]
|
||||
name = "kratactl"
|
||||
path = "bin/control.rs"
|
@ -1,11 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use env_logger::Env;
|
||||
|
||||
use kratactl::cli::ControlCommand;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
|
||||
ControlCommand::parse().run().await
|
||||
}
|
@ -1,128 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use comfy_table::{presets::UTF8_FULL_CONDENSED, Cell, Color, Table};
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::control::{control_service_client::ControlServiceClient, DeviceInfo, ListDevicesRequest},
|
||||
};
|
||||
|
||||
use serde_json::Value;
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use crate::format::{kv2line, proto2dynamic, proto2kv};
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
|
||||
enum DeviceListFormat {
|
||||
Table,
|
||||
Json,
|
||||
JsonPretty,
|
||||
Jsonl,
|
||||
Yaml,
|
||||
KeyValue,
|
||||
Simple,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "List device information")]
|
||||
pub struct DeviceListCommand {
|
||||
#[arg(short, long, default_value = "table", help = "Output format")]
|
||||
format: DeviceListFormat,
|
||||
}
|
||||
|
||||
impl DeviceListCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
_events: EventStream,
|
||||
) -> Result<()> {
|
||||
let reply = client
|
||||
.list_devices(ListDevicesRequest {})
|
||||
.await?
|
||||
.into_inner();
|
||||
let mut devices = reply.devices;
|
||||
|
||||
devices.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
|
||||
match self.format {
|
||||
DeviceListFormat::Table => {
|
||||
self.print_devices_table(devices)?;
|
||||
}
|
||||
|
||||
DeviceListFormat::Simple => {
|
||||
for device in devices {
|
||||
println!("{}\t{}\t{}", device.name, device.claimed, device.owner);
|
||||
}
|
||||
}
|
||||
|
||||
DeviceListFormat::Json | DeviceListFormat::JsonPretty | DeviceListFormat::Yaml => {
|
||||
let mut values = Vec::new();
|
||||
for device in devices {
|
||||
let message = proto2dynamic(device)?;
|
||||
values.push(serde_json::to_value(message)?);
|
||||
}
|
||||
let value = Value::Array(values);
|
||||
let encoded = if self.format == DeviceListFormat::JsonPretty {
|
||||
serde_json::to_string_pretty(&value)?
|
||||
} else if self.format == DeviceListFormat::Yaml {
|
||||
serde_yaml::to_string(&value)?
|
||||
} else {
|
||||
serde_json::to_string(&value)?
|
||||
};
|
||||
println!("{}", encoded.trim());
|
||||
}
|
||||
|
||||
DeviceListFormat::Jsonl => {
|
||||
for device in devices {
|
||||
let message = proto2dynamic(device)?;
|
||||
println!("{}", serde_json::to_string(&message)?);
|
||||
}
|
||||
}
|
||||
|
||||
DeviceListFormat::KeyValue => {
|
||||
self.print_key_value(devices)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_devices_table(&self, devices: Vec<DeviceInfo>) -> Result<()> {
|
||||
let mut table = Table::new();
|
||||
table.load_preset(UTF8_FULL_CONDENSED);
|
||||
table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
|
||||
table.set_header(vec!["name", "status", "owner"]);
|
||||
for device in devices {
|
||||
let status_text = if device.claimed {
|
||||
"claimed"
|
||||
} else {
|
||||
"available"
|
||||
};
|
||||
|
||||
let status_color = if device.claimed {
|
||||
Color::Blue
|
||||
} else {
|
||||
Color::Green
|
||||
};
|
||||
|
||||
table.add_row(vec![
|
||||
Cell::new(device.name),
|
||||
Cell::new(status_text).fg(status_color),
|
||||
Cell::new(device.owner),
|
||||
]);
|
||||
}
|
||||
if table.is_empty() {
|
||||
println!("no devices configured");
|
||||
} else {
|
||||
println!("{}", table);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_key_value(&self, devices: Vec<DeviceInfo>) -> Result<()> {
|
||||
for device in devices {
|
||||
let kvs = proto2kv(device)?;
|
||||
println!("{}", kv2line(kvs));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use krata::events::EventStream;
|
||||
use krata::v1::control::control_service_client::ControlServiceClient;
|
||||
|
||||
use crate::cli::device::list::DeviceListCommand;
|
||||
|
||||
pub mod list;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Manage the devices on the isolation engine")]
|
||||
pub struct DeviceCommand {
|
||||
#[command(subcommand)]
|
||||
subcommand: DeviceCommands,
|
||||
}
|
||||
|
||||
impl DeviceCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
self.subcommand.run(client, events).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum DeviceCommands {
|
||||
List(DeviceListCommand),
|
||||
}
|
||||
|
||||
impl DeviceCommands {
|
||||
pub async fn run(
|
||||
self,
|
||||
client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
match self {
|
||||
DeviceCommands::List(list) => list.run(client, events).await,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,104 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use comfy_table::presets::UTF8_FULL_CONDENSED;
|
||||
use comfy_table::{Cell, Table};
|
||||
use krata::v1::control::{
|
||||
control_service_client::ControlServiceClient, GetHostCpuTopologyRequest, HostCpuTopologyClass,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::format::{kv2line, proto2dynamic, proto2kv};
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
fn class_to_str(input: HostCpuTopologyClass) -> String {
|
||||
match input {
|
||||
HostCpuTopologyClass::Standard => "Standard".to_string(),
|
||||
HostCpuTopologyClass::Performance => "Performance".to_string(),
|
||||
HostCpuTopologyClass::Efficiency => "Efficiency".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
|
||||
enum HostCpuTopologyFormat {
|
||||
Table,
|
||||
Json,
|
||||
JsonPretty,
|
||||
Jsonl,
|
||||
Yaml,
|
||||
KeyValue,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Display information about the host CPU topology")]
|
||||
pub struct HostCpuTopologyCommand {
|
||||
#[arg(short, long, default_value = "table", help = "Output format")]
|
||||
format: HostCpuTopologyFormat,
|
||||
}
|
||||
|
||||
impl HostCpuTopologyCommand {
|
||||
pub async fn run(self, mut client: ControlServiceClient<Channel>) -> Result<()> {
|
||||
let response = client
|
||||
.get_host_cpu_topology(Request::new(GetHostCpuTopologyRequest {}))
|
||||
.await?
|
||||
.into_inner();
|
||||
|
||||
match self.format {
|
||||
HostCpuTopologyFormat::Table => {
|
||||
let mut table = Table::new();
|
||||
table.load_preset(UTF8_FULL_CONDENSED);
|
||||
table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
|
||||
table.set_header(vec!["id", "node", "socket", "core", "thread", "class"]);
|
||||
|
||||
for (i, cpu) in response.cpus.iter().enumerate() {
|
||||
table.add_row(vec![
|
||||
Cell::new(i),
|
||||
Cell::new(cpu.node),
|
||||
Cell::new(cpu.socket),
|
||||
Cell::new(cpu.core),
|
||||
Cell::new(cpu.thread),
|
||||
Cell::new(class_to_str(cpu.class())),
|
||||
]);
|
||||
}
|
||||
|
||||
if !table.is_empty() {
|
||||
println!("{}", table);
|
||||
}
|
||||
}
|
||||
|
||||
HostCpuTopologyFormat::Json
|
||||
| HostCpuTopologyFormat::JsonPretty
|
||||
| HostCpuTopologyFormat::Yaml => {
|
||||
let mut values = Vec::new();
|
||||
for cpu in response.cpus {
|
||||
let message = proto2dynamic(cpu)?;
|
||||
values.push(serde_json::to_value(message)?);
|
||||
}
|
||||
let value = Value::Array(values);
|
||||
let encoded = if self.format == HostCpuTopologyFormat::JsonPretty {
|
||||
serde_json::to_string_pretty(&value)?
|
||||
} else if self.format == HostCpuTopologyFormat::Yaml {
|
||||
serde_yaml::to_string(&value)?
|
||||
} else {
|
||||
serde_json::to_string(&value)?
|
||||
};
|
||||
println!("{}", encoded.trim());
|
||||
}
|
||||
|
||||
HostCpuTopologyFormat::Jsonl => {
|
||||
for cpu in response.cpus {
|
||||
let message = proto2dynamic(cpu)?;
|
||||
println!("{}", serde_json::to_string(&message)?);
|
||||
}
|
||||
}
|
||||
|
||||
HostCpuTopologyFormat::KeyValue => {
|
||||
for cpu in response.cpus {
|
||||
let kvs = proto2kv(cpu)?;
|
||||
println!("{}", kv2line(kvs),);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use krata::v1::control::{
|
||||
control_service_client::ControlServiceClient, ReadHypervisorConsoleRequest,
|
||||
};
|
||||
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Display hypervisor console output")]
|
||||
pub struct HostHvConsoleCommand {}
|
||||
|
||||
impl HostHvConsoleCommand {
|
||||
pub async fn run(self, mut client: ControlServiceClient<Channel>) -> Result<()> {
|
||||
let response = client
|
||||
.read_hypervisor_console(Request::new(ReadHypervisorConsoleRequest {}))
|
||||
.await?
|
||||
.into_inner();
|
||||
|
||||
print!("{}", response.data);
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,157 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use base64::Engine;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
idm::{internal, serialize::IdmSerializable, transport::IdmTransportPacketForm},
|
||||
v1::control::{control_service_client::ControlServiceClient, SnoopIdmReply, SnoopIdmRequest},
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use tokio_stream::StreamExt;
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use crate::format::{kv2line, proto2dynamic, value2kv};
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
|
||||
enum HostIdmSnoopFormat {
|
||||
Simple,
|
||||
Jsonl,
|
||||
KeyValue,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Snoop on the IDM bus")]
|
||||
pub struct HostIdmSnoopCommand {
|
||||
#[arg(short, long, default_value = "simple", help = "Output format")]
|
||||
format: HostIdmSnoopFormat,
|
||||
}
|
||||
|
||||
impl HostIdmSnoopCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
_events: EventStream,
|
||||
) -> Result<()> {
|
||||
let mut stream = client.snoop_idm(SnoopIdmRequest {}).await?.into_inner();
|
||||
|
||||
while let Some(reply) = stream.next().await {
|
||||
let reply = reply?;
|
||||
let Some(line) = convert_idm_snoop(reply) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match self.format {
|
||||
HostIdmSnoopFormat::Simple => {
|
||||
self.print_simple(line)?;
|
||||
}
|
||||
|
||||
HostIdmSnoopFormat::Jsonl => {
|
||||
let encoded = serde_json::to_string(&line)?;
|
||||
println!("{}", encoded.trim());
|
||||
}
|
||||
|
||||
HostIdmSnoopFormat::KeyValue => {
|
||||
self.print_key_value(line)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_simple(&self, line: IdmSnoopLine) -> Result<()> {
|
||||
let encoded = if !line.packet.decoded.is_null() {
|
||||
serde_json::to_string(&line.packet.decoded)?
|
||||
} else {
|
||||
base64::prelude::BASE64_STANDARD.encode(&line.packet.data)
|
||||
};
|
||||
println!(
|
||||
"({} -> {}) {} {} {}",
|
||||
line.from, line.to, line.packet.id, line.packet.form, encoded
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_key_value(&self, line: IdmSnoopLine) -> Result<()> {
|
||||
let kvs = value2kv(serde_json::to_value(line)?)?;
|
||||
println!("{}", kv2line(kvs));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct IdmSnoopLine {
|
||||
pub from: String,
|
||||
pub to: String,
|
||||
pub packet: IdmSnoopData,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct IdmSnoopData {
|
||||
pub id: u64,
|
||||
pub channel: u64,
|
||||
pub form: String,
|
||||
pub data: String,
|
||||
pub decoded: Value,
|
||||
}
|
||||
|
||||
pub fn convert_idm_snoop(reply: SnoopIdmReply) -> Option<IdmSnoopLine> {
|
||||
let packet = &(reply.packet?);
|
||||
|
||||
let decoded = if packet.channel == 0 {
|
||||
match packet.form() {
|
||||
IdmTransportPacketForm::Event => internal::Event::decode(&packet.data)
|
||||
.ok()
|
||||
.and_then(|event| proto2dynamic(event).ok()),
|
||||
|
||||
IdmTransportPacketForm::Request
|
||||
| IdmTransportPacketForm::StreamRequest
|
||||
| IdmTransportPacketForm::StreamRequestUpdate => {
|
||||
internal::Request::decode(&packet.data)
|
||||
.ok()
|
||||
.and_then(|event| proto2dynamic(event).ok())
|
||||
}
|
||||
|
||||
IdmTransportPacketForm::Response | IdmTransportPacketForm::StreamResponseUpdate => {
|
||||
internal::Response::decode(&packet.data)
|
||||
.ok()
|
||||
.and_then(|event| proto2dynamic(event).ok())
|
||||
}
|
||||
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let decoded = decoded
|
||||
.and_then(|message| serde_json::to_value(message).ok())
|
||||
.unwrap_or(Value::Null);
|
||||
|
||||
let data = IdmSnoopData {
|
||||
id: packet.id,
|
||||
channel: packet.channel,
|
||||
form: match packet.form() {
|
||||
IdmTransportPacketForm::Raw => "raw".to_string(),
|
||||
IdmTransportPacketForm::Event => "event".to_string(),
|
||||
IdmTransportPacketForm::Request => "request".to_string(),
|
||||
IdmTransportPacketForm::Response => "response".to_string(),
|
||||
IdmTransportPacketForm::StreamRequest => "stream-request".to_string(),
|
||||
IdmTransportPacketForm::StreamRequestUpdate => "stream-request-update".to_string(),
|
||||
IdmTransportPacketForm::StreamRequestClosed => "stream-request-closed".to_string(),
|
||||
IdmTransportPacketForm::StreamResponseUpdate => "stream-response-update".to_string(),
|
||||
IdmTransportPacketForm::StreamResponseClosed => "stream-response-closed".to_string(),
|
||||
_ => format!("unknown-{}", packet.form),
|
||||
},
|
||||
data: base64::prelude::BASE64_STANDARD.encode(&packet.data),
|
||||
decoded,
|
||||
};
|
||||
|
||||
Some(IdmSnoopLine {
|
||||
from: reply.from,
|
||||
to: reply.to,
|
||||
packet: data,
|
||||
})
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use krata::events::EventStream;
|
||||
use krata::v1::control::control_service_client::ControlServiceClient;
|
||||
|
||||
use crate::cli::host::cpu_topology::HostCpuTopologyCommand;
|
||||
use crate::cli::host::hv_console::HostHvConsoleCommand;
|
||||
use crate::cli::host::idm_snoop::HostIdmSnoopCommand;
|
||||
use crate::cli::host::status::HostStatusCommand;
|
||||
|
||||
pub mod cpu_topology;
|
||||
pub mod hv_console;
|
||||
pub mod idm_snoop;
|
||||
pub mod status;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Manage the host of the isolation engine")]
|
||||
pub struct HostCommand {
|
||||
#[command(subcommand)]
|
||||
subcommand: HostCommands,
|
||||
}
|
||||
|
||||
impl HostCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
self.subcommand.run(client, events).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum HostCommands {
|
||||
CpuTopology(HostCpuTopologyCommand),
|
||||
Status(HostStatusCommand),
|
||||
IdmSnoop(HostIdmSnoopCommand),
|
||||
HvConsole(HostHvConsoleCommand),
|
||||
}
|
||||
|
||||
impl HostCommands {
|
||||
pub async fn run(
|
||||
self,
|
||||
client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
match self {
|
||||
HostCommands::CpuTopology(cpu_topology) => cpu_topology.run(client).await,
|
||||
|
||||
HostCommands::Status(status) => status.run(client).await,
|
||||
|
||||
HostCommands::IdmSnoop(snoop) => snoop.run(client, events).await,
|
||||
|
||||
HostCommands::HvConsole(hvconsole) => hvconsole.run(client).await,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use krata::v1::control::{control_service_client::ControlServiceClient, GetHostStatusRequest};
|
||||
|
||||
use crate::format::{kv2line, proto2dynamic, proto2kv};
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
|
||||
enum HostStatusFormat {
|
||||
Simple,
|
||||
Json,
|
||||
JsonPretty,
|
||||
Yaml,
|
||||
KeyValue,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Get information about the host")]
|
||||
pub struct HostStatusCommand {
|
||||
#[arg(short, long, default_value = "simple", help = "Output format")]
|
||||
format: HostStatusFormat,
|
||||
}
|
||||
|
||||
impl HostStatusCommand {
|
||||
pub async fn run(self, mut client: ControlServiceClient<Channel>) -> Result<()> {
|
||||
let response = client
|
||||
.get_host_status(Request::new(GetHostStatusRequest {}))
|
||||
.await?
|
||||
.into_inner();
|
||||
match self.format {
|
||||
HostStatusFormat::Simple => {
|
||||
println!("Host UUID: {}", response.host_uuid);
|
||||
println!("Host Domain: {}", response.host_domid);
|
||||
println!("Krata Version: {}", response.krata_version);
|
||||
println!("Host IPv4: {}", response.host_ipv4);
|
||||
println!("Host IPv6: {}", response.host_ipv6);
|
||||
println!("Host Ethernet Address: {}", response.host_mac);
|
||||
}
|
||||
|
||||
HostStatusFormat::Json | HostStatusFormat::JsonPretty | HostStatusFormat::Yaml => {
|
||||
let message = proto2dynamic(response)?;
|
||||
let value = serde_json::to_value(message)?;
|
||||
let encoded = if self.format == HostStatusFormat::JsonPretty {
|
||||
serde_json::to_string_pretty(&value)?
|
||||
} else if self.format == HostStatusFormat::Yaml {
|
||||
serde_yaml::to_string(&value)?
|
||||
} else {
|
||||
serde_json::to_string(&value)?
|
||||
};
|
||||
println!("{}", encoded.trim());
|
||||
}
|
||||
|
||||
HostStatusFormat::KeyValue => {
|
||||
let kvs = proto2kv(response)?;
|
||||
println!("{}", kv2line(kvs),);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use krata::events::EventStream;
|
||||
use krata::v1::control::control_service_client::ControlServiceClient;
|
||||
|
||||
use crate::cli::image::pull::ImagePullCommand;
|
||||
|
||||
pub mod pull;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Manage the images on the isolation engine")]
|
||||
pub struct ImageCommand {
|
||||
#[command(subcommand)]
|
||||
subcommand: ImageCommands,
|
||||
}
|
||||
|
||||
impl ImageCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
self.subcommand.run(client, events).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum ImageCommands {
|
||||
Pull(ImagePullCommand),
|
||||
}
|
||||
|
||||
impl ImageCommands {
|
||||
pub async fn run(
|
||||
self,
|
||||
client: ControlServiceClient<Channel>,
|
||||
_events: EventStream,
|
||||
) -> Result<()> {
|
||||
match self {
|
||||
ImageCommands::Pull(pull) => pull.run(client).await,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use krata::v1::{
|
||||
common::OciImageFormat,
|
||||
control::{control_service_client::ControlServiceClient, PullImageRequest},
|
||||
};
|
||||
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use crate::pull::pull_interactive_progress;
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ImagePullImageFormat {
|
||||
Squashfs,
|
||||
Erofs,
|
||||
Tar,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Pull an image into the cache")]
|
||||
pub struct ImagePullCommand {
|
||||
#[arg(help = "Image name")]
|
||||
image: String,
|
||||
#[arg(short = 's', long, default_value = "squashfs", help = "Image format")]
|
||||
image_format: ImagePullImageFormat,
|
||||
#[arg(short = 'o', long, help = "Overwrite image cache")]
|
||||
overwrite_cache: bool,
|
||||
}
|
||||
|
||||
impl ImagePullCommand {
|
||||
pub async fn run(self, mut client: ControlServiceClient<Channel>) -> Result<()> {
|
||||
let response = client
|
||||
.pull_image(PullImageRequest {
|
||||
image: self.image.clone(),
|
||||
format: match self.image_format {
|
||||
ImagePullImageFormat::Squashfs => OciImageFormat::Squashfs.into(),
|
||||
ImagePullImageFormat::Erofs => OciImageFormat::Erofs.into(),
|
||||
ImagePullImageFormat::Tar => OciImageFormat::Tar.into(),
|
||||
},
|
||||
overwrite_cache: self.overwrite_cache,
|
||||
update: true,
|
||||
})
|
||||
.await?;
|
||||
let reply = pull_interactive_progress(response.into_inner()).await?;
|
||||
println!("{}", reply.digest);
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
pub mod device;
|
||||
pub mod host;
|
||||
pub mod image;
|
||||
pub mod network;
|
||||
pub mod zone;
|
||||
|
||||
use crate::cli::device::DeviceCommand;
|
||||
use crate::cli::host::HostCommand;
|
||||
use crate::cli::image::ImageCommand;
|
||||
use crate::cli::zone::ZoneCommand;
|
||||
use anyhow::{anyhow, Result};
|
||||
use clap::Parser;
|
||||
use krata::{
|
||||
client::ControlClientProvider,
|
||||
events::EventStream,
|
||||
v1::control::{control_service_client::ControlServiceClient, ResolveZoneIdRequest},
|
||||
};
|
||||
use network::NetworkCommand;
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(version, about = "Control the krata isolation engine")]
|
||||
pub struct ControlCommand {
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
help = "The connection URL to the krata isolation engine",
|
||||
default_value = "unix:///var/lib/krata/daemon.socket"
|
||||
)]
|
||||
connection: String,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: ControlCommands,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Parser)]
|
||||
pub enum ControlCommands {
|
||||
Zone(ZoneCommand),
|
||||
Image(ImageCommand),
|
||||
Network(NetworkCommand),
|
||||
Device(DeviceCommand),
|
||||
Host(HostCommand),
|
||||
}
|
||||
|
||||
impl ControlCommand {
|
||||
pub async fn run(self) -> Result<()> {
|
||||
let client = ControlClientProvider::dial(self.connection.parse()?).await?;
|
||||
let events = EventStream::open(client.clone()).await?;
|
||||
self.command.run(client, events).await
|
||||
}
|
||||
}
|
||||
|
||||
impl ControlCommands {
|
||||
pub async fn run(
|
||||
self,
|
||||
client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
match self {
|
||||
ControlCommands::Zone(zone) => zone.run(client, events).await,
|
||||
|
||||
ControlCommands::Network(network) => network.run(client, events).await,
|
||||
|
||||
ControlCommands::Image(image) => image.run(client, events).await,
|
||||
|
||||
ControlCommands::Device(device) => device.run(client, events).await,
|
||||
|
||||
ControlCommands::Host(host) => host.run(client, events).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn resolve_zone(
|
||||
client: &mut ControlServiceClient<Channel>,
|
||||
name: &str,
|
||||
) -> Result<String> {
|
||||
let reply = client
|
||||
.resolve_zone_id(Request::new(ResolveZoneIdRequest {
|
||||
name: name.to_string(),
|
||||
}))
|
||||
.await?
|
||||
.into_inner();
|
||||
|
||||
if !reply.zone_id.is_empty() {
|
||||
Ok(reply.zone_id)
|
||||
} else {
|
||||
Err(anyhow!("unable to resolve zone '{}'", name))
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use reservation::NetworkReservationCommand;
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use krata::events::EventStream;
|
||||
use krata::v1::control::control_service_client::ControlServiceClient;
|
||||
|
||||
pub mod reservation;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Manage the network on the isolation engine")]
|
||||
pub struct NetworkCommand {
|
||||
#[command(subcommand)]
|
||||
subcommand: NetworkCommands,
|
||||
}
|
||||
|
||||
impl NetworkCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
self.subcommand.run(client, events).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum NetworkCommands {
|
||||
Reservation(NetworkReservationCommand),
|
||||
}
|
||||
|
||||
impl NetworkCommands {
|
||||
pub async fn run(
|
||||
self,
|
||||
client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
match self {
|
||||
NetworkCommands::Reservation(reservation) => reservation.run(client, events).await,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,125 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use comfy_table::{presets::UTF8_FULL_CONDENSED, Cell, Table};
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::{
|
||||
common::NetworkReservation,
|
||||
control::{control_service_client::ControlServiceClient, ListNetworkReservationsRequest},
|
||||
},
|
||||
};
|
||||
|
||||
use serde_json::Value;
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use crate::format::{kv2line, proto2dynamic, proto2kv};
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
|
||||
enum NetworkReservationListFormat {
|
||||
Table,
|
||||
Json,
|
||||
JsonPretty,
|
||||
Jsonl,
|
||||
Yaml,
|
||||
KeyValue,
|
||||
Simple,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "List network reservation information")]
|
||||
pub struct NetworkReservationListCommand {
|
||||
#[arg(short, long, default_value = "table", help = "Output format")]
|
||||
format: NetworkReservationListFormat,
|
||||
}
|
||||
|
||||
impl NetworkReservationListCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
_events: EventStream,
|
||||
) -> Result<()> {
|
||||
let reply = client
|
||||
.list_network_reservations(ListNetworkReservationsRequest {})
|
||||
.await?
|
||||
.into_inner();
|
||||
let mut reservations = reply.reservations;
|
||||
|
||||
reservations.sort_by(|a, b| a.uuid.cmp(&b.uuid));
|
||||
|
||||
match self.format {
|
||||
NetworkReservationListFormat::Table => {
|
||||
self.print_reservations_table(reservations)?;
|
||||
}
|
||||
|
||||
NetworkReservationListFormat::Simple => {
|
||||
for reservation in reservations {
|
||||
println!(
|
||||
"{}\t{}\t{}\t{}",
|
||||
reservation.uuid, reservation.ipv4, reservation.ipv6, reservation.mac
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NetworkReservationListFormat::Json
|
||||
| NetworkReservationListFormat::JsonPretty
|
||||
| NetworkReservationListFormat::Yaml => {
|
||||
let mut values = Vec::new();
|
||||
for device in reservations {
|
||||
let message = proto2dynamic(device)?;
|
||||
values.push(serde_json::to_value(message)?);
|
||||
}
|
||||
let value = Value::Array(values);
|
||||
let encoded = if self.format == NetworkReservationListFormat::JsonPretty {
|
||||
serde_json::to_string_pretty(&value)?
|
||||
} else if self.format == NetworkReservationListFormat::Yaml {
|
||||
serde_yaml::to_string(&value)?
|
||||
} else {
|
||||
serde_json::to_string(&value)?
|
||||
};
|
||||
println!("{}", encoded.trim());
|
||||
}
|
||||
|
||||
NetworkReservationListFormat::Jsonl => {
|
||||
for device in reservations {
|
||||
let message = proto2dynamic(device)?;
|
||||
println!("{}", serde_json::to_string(&message)?);
|
||||
}
|
||||
}
|
||||
|
||||
NetworkReservationListFormat::KeyValue => {
|
||||
self.print_key_value(reservations)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_reservations_table(&self, reservations: Vec<NetworkReservation>) -> Result<()> {
|
||||
let mut table = Table::new();
|
||||
table.load_preset(UTF8_FULL_CONDENSED);
|
||||
table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
|
||||
table.set_header(vec!["uuid", "ipv4", "ipv6", "mac"]);
|
||||
for reservation in reservations {
|
||||
table.add_row(vec![
|
||||
Cell::new(reservation.uuid),
|
||||
Cell::new(reservation.ipv4),
|
||||
Cell::new(reservation.ipv6),
|
||||
Cell::new(reservation.mac),
|
||||
]);
|
||||
}
|
||||
if table.is_empty() {
|
||||
println!("no network reservations found");
|
||||
} else {
|
||||
println!("{}", table);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_key_value(&self, reservations: Vec<NetworkReservation>) -> Result<()> {
|
||||
for reservation in reservations {
|
||||
let kvs = proto2kv(reservation)?;
|
||||
println!("{}", kv2line(kvs));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use list::NetworkReservationListCommand;
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use krata::events::EventStream;
|
||||
use krata::v1::control::control_service_client::ControlServiceClient;
|
||||
|
||||
pub mod list;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Manage network reservations")]
|
||||
pub struct NetworkReservationCommand {
|
||||
#[command(subcommand)]
|
||||
subcommand: NetworkReservationCommands,
|
||||
}
|
||||
|
||||
impl NetworkReservationCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
self.subcommand.run(client, events).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum NetworkReservationCommands {
|
||||
List(NetworkReservationListCommand),
|
||||
}
|
||||
|
||||
impl NetworkReservationCommands {
|
||||
pub async fn run(
|
||||
self,
|
||||
client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
match self {
|
||||
NetworkReservationCommands::List(list) => list.run(client, events).await,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use krata::{events::EventStream, v1::control::control_service_client::ControlServiceClient};
|
||||
|
||||
use tokio::select;
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use crate::console::StdioConsoleStream;
|
||||
|
||||
use crate::cli::resolve_zone;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Attach to the zone console")]
|
||||
pub struct ZoneAttachCommand {
|
||||
#[arg(help = "Zone to attach to, either the name or the uuid")]
|
||||
zone: String,
|
||||
}
|
||||
|
||||
impl ZoneAttachCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
let zone_id: String = resolve_zone(&mut client, &self.zone).await?;
|
||||
let input = StdioConsoleStream::stdin_stream(zone_id.clone(), false).await;
|
||||
let output = client.attach_zone_console(input).await?.into_inner();
|
||||
let stdout_handle =
|
||||
tokio::task::spawn(async move { StdioConsoleStream::stdout(output, true).await });
|
||||
let exit_hook_task = StdioConsoleStream::zone_exit_hook(zone_id.clone(), events).await?;
|
||||
let code = select! {
|
||||
x = stdout_handle => {
|
||||
x??;
|
||||
None
|
||||
},
|
||||
x = exit_hook_task => x?
|
||||
};
|
||||
StdioConsoleStream::restore_terminal_mode();
|
||||
std::process::exit(code.unwrap_or(0));
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::control::{
|
||||
control_service_client::ControlServiceClient, watch_events_reply::Event, DestroyZoneRequest,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::cli::resolve_zone;
|
||||
use krata::v1::common::ZoneState;
|
||||
use log::error;
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Destroy a zone")]
|
||||
pub struct ZoneDestroyCommand {
|
||||
#[arg(
|
||||
short = 'W',
|
||||
long,
|
||||
help = "Wait for the destruction of the zone to complete"
|
||||
)]
|
||||
wait: bool,
|
||||
#[arg(help = "Zone to destroy, either the name or the uuid")]
|
||||
zone: String,
|
||||
}
|
||||
|
||||
impl ZoneDestroyCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
let zone_id: String = resolve_zone(&mut client, &self.zone).await?;
|
||||
let _ = client
|
||||
.destroy_zone(Request::new(DestroyZoneRequest {
|
||||
zone_id: zone_id.clone(),
|
||||
}))
|
||||
.await?
|
||||
.into_inner();
|
||||
if self.wait {
|
||||
wait_zone_destroyed(&zone_id, events).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_zone_destroyed(id: &str, events: EventStream) -> Result<()> {
|
||||
let mut stream = events.subscribe();
|
||||
while let Ok(event) = stream.recv().await {
|
||||
let Event::ZoneChanged(changed) = event;
|
||||
let Some(zone) = changed.zone else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if zone.id != id {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(status) = zone.status else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(ref error) = status.error_status {
|
||||
if status.state() == ZoneState::Failed {
|
||||
error!("destroy failed: {}", error.message);
|
||||
std::process::exit(1);
|
||||
} else {
|
||||
error!("zone error: {}", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if status.state() == ZoneState::Destroyed {
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use clap::Parser;
|
||||
use crossterm::tty::IsTty;
|
||||
use krata::v1::{
|
||||
common::{TerminalSize, ZoneTaskSpec, ZoneTaskSpecEnvVar},
|
||||
control::{control_service_client::ControlServiceClient, ExecInsideZoneRequest},
|
||||
};
|
||||
|
||||
use tokio::io::stdin;
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
use crate::console::StdioConsoleStream;
|
||||
|
||||
use crate::cli::resolve_zone;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Execute a command inside the zone")]
|
||||
pub struct ZoneExecCommand {
|
||||
#[arg[short, long, help = "Environment variables"]]
|
||||
env: Option<Vec<String>>,
|
||||
#[arg(short = 'w', long, help = "Working directory")]
|
||||
working_directory: Option<String>,
|
||||
#[arg(short = 't', long, help = "Allocate tty")]
|
||||
tty: bool,
|
||||
#[arg(help = "Zone to exec inside, either the name or the uuid")]
|
||||
zone: String,
|
||||
#[arg(
|
||||
allow_hyphen_values = true,
|
||||
trailing_var_arg = true,
|
||||
help = "Command to run inside the zone"
|
||||
)]
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
impl ZoneExecCommand {
|
||||
pub async fn run(self, mut client: ControlServiceClient<Channel>) -> Result<()> {
|
||||
let zone_id: String = resolve_zone(&mut client, &self.zone).await?;
|
||||
let should_map_tty = self.tty && stdin().is_tty();
|
||||
let initial = ExecInsideZoneRequest {
|
||||
zone_id,
|
||||
task: Some(ZoneTaskSpec {
|
||||
environment: env_map(&self.env.unwrap_or_default())
|
||||
.iter()
|
||||
.map(|(key, value)| ZoneTaskSpecEnvVar {
|
||||
key: key.clone(),
|
||||
value: value.clone(),
|
||||
})
|
||||
.collect(),
|
||||
command: self.command,
|
||||
working_directory: self.working_directory.unwrap_or_default(),
|
||||
tty: self.tty,
|
||||
}),
|
||||
stdin: vec![],
|
||||
stdin_closed: false,
|
||||
terminal_size: if should_map_tty {
|
||||
let size = crossterm::terminal::size().ok();
|
||||
size.map(|(columns, rows)| TerminalSize {
|
||||
rows: rows as u32,
|
||||
columns: columns as u32,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
},
|
||||
};
|
||||
|
||||
let stream = StdioConsoleStream::input_stream_exec(initial, should_map_tty).await;
|
||||
|
||||
let response = client
|
||||
.exec_inside_zone(Request::new(stream))
|
||||
.await?
|
||||
.into_inner();
|
||||
|
||||
let code = StdioConsoleStream::exec_output(response, should_map_tty).await?;
|
||||
std::process::exit(code);
|
||||
}
|
||||
}
|
||||
|
||||
fn env_map(env: &[String]) -> HashMap<String, String> {
|
||||
let mut map = HashMap::<String, String>::new();
|
||||
for item in env {
|
||||
if let Some((key, value)) = item.split_once('=') {
|
||||
map.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
@ -1,282 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::{
|
||||
common::{
|
||||
zone_image_spec::Image, OciImageFormat, ZoneImageSpec, ZoneKernelOptionsSpec,
|
||||
ZoneOciImageSpec, ZoneResourceSpec, ZoneSpec, ZoneSpecDevice, ZoneState, ZoneTaskSpec,
|
||||
ZoneTaskSpecEnvVar,
|
||||
},
|
||||
control::{
|
||||
control_service_client::ControlServiceClient, watch_events_reply::Event,
|
||||
CreateZoneRequest, PullImageRequest,
|
||||
},
|
||||
},
|
||||
};
|
||||
use log::error;
|
||||
use tokio::select;
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
use crate::{console::StdioConsoleStream, pull::pull_interactive_progress};
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LaunchImageFormat {
|
||||
Squashfs,
|
||||
Erofs,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Launch a new zone")]
|
||||
pub struct ZoneLaunchCommand {
|
||||
#[arg(long, default_value = "squashfs", help = "Image format")]
|
||||
image_format: LaunchImageFormat,
|
||||
#[arg(long, help = "Overwrite image cache on pull")]
|
||||
pull_overwrite_cache: bool,
|
||||
#[arg(long, help = "Update image on pull")]
|
||||
pull_update: bool,
|
||||
#[arg(short, long, help = "Name of the zone")]
|
||||
name: Option<String>,
|
||||
#[arg(
|
||||
short = 'C',
|
||||
long = "max-cpus",
|
||||
default_value_t = 4,
|
||||
help = "Maximum vCPUs available for the zone"
|
||||
)]
|
||||
max_cpus: u32,
|
||||
#[arg(
|
||||
short = 'c',
|
||||
long = "target-cpus",
|
||||
default_value_t = 1,
|
||||
help = "Target vCPUs for the zone to use"
|
||||
)]
|
||||
target_cpus: u32,
|
||||
#[arg(
|
||||
short = 'M',
|
||||
long = "max-memory",
|
||||
default_value_t = 1024,
|
||||
help = "Maximum memory available to the zone, in megabytes"
|
||||
)]
|
||||
max_memory: u64,
|
||||
#[arg(
|
||||
short = 'm',
|
||||
long = "target-memory",
|
||||
default_value_t = 1024,
|
||||
help = "Target memory for the zone to use, in megabytes"
|
||||
)]
|
||||
target_memory: u64,
|
||||
#[arg[short = 'D', long = "device", help = "Devices to request for the zone"]]
|
||||
device: Vec<String>,
|
||||
#[arg[short, long, help = "Environment variables set in the zone"]]
|
||||
env: Option<Vec<String>>,
|
||||
#[arg(short = 't', long, help = "Allocate tty for task")]
|
||||
tty: bool,
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
help = "Attach to the zone after zone starts, implies --wait"
|
||||
)]
|
||||
attach: bool,
|
||||
#[arg(
|
||||
short = 'W',
|
||||
long,
|
||||
help = "Wait for the zone to start, implied by --attach"
|
||||
)]
|
||||
wait: bool,
|
||||
#[arg(short = 'k', long, help = "OCI kernel image for zone to use")]
|
||||
kernel: Option<String>,
|
||||
#[arg(short = 'I', long, help = "OCI initrd image for zone to use")]
|
||||
initrd: Option<String>,
|
||||
#[arg(short = 'w', long, help = "Working directory")]
|
||||
working_directory: Option<String>,
|
||||
#[arg(long, help = "Enable verbose logging on the kernel")]
|
||||
kernel_verbose: bool,
|
||||
#[arg(long, help = "Additional kernel cmdline options")]
|
||||
kernel_cmdline_append: Option<String>,
|
||||
#[arg(help = "Container image for zone to use")]
|
||||
oci: String,
|
||||
#[arg(
|
||||
allow_hyphen_values = true,
|
||||
trailing_var_arg = true,
|
||||
help = "Command to run inside the zone"
|
||||
)]
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
impl ZoneLaunchCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
let image = self
|
||||
.pull_image(
|
||||
&mut client,
|
||||
&self.oci,
|
||||
match self.image_format {
|
||||
LaunchImageFormat::Squashfs => OciImageFormat::Squashfs,
|
||||
LaunchImageFormat::Erofs => OciImageFormat::Erofs,
|
||||
},
|
||||
)
|
||||
.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 = CreateZoneRequest {
|
||||
spec: Some(ZoneSpec {
|
||||
name: self.name.unwrap_or_default(),
|
||||
image: Some(image),
|
||||
kernel,
|
||||
initrd,
|
||||
initial_resources: Some(ZoneResourceSpec {
|
||||
max_memory: self.max_memory,
|
||||
target_memory: self.target_memory,
|
||||
max_cpus: self.max_cpus,
|
||||
target_cpus: self.target_cpus,
|
||||
}),
|
||||
task: Some(ZoneTaskSpec {
|
||||
environment: env_map(&self.env.unwrap_or_default())
|
||||
.iter()
|
||||
.map(|(key, value)| ZoneTaskSpecEnvVar {
|
||||
key: key.clone(),
|
||||
value: value.clone(),
|
||||
})
|
||||
.collect(),
|
||||
command: self.command,
|
||||
working_directory: self.working_directory.unwrap_or_default(),
|
||||
tty: self.tty,
|
||||
}),
|
||||
annotations: vec![],
|
||||
devices: self
|
||||
.device
|
||||
.iter()
|
||||
.map(|name| ZoneSpecDevice { name: name.clone() })
|
||||
.collect(),
|
||||
kernel_options: Some(ZoneKernelOptionsSpec {
|
||||
verbose: self.kernel_verbose,
|
||||
cmdline_append: self.kernel_cmdline_append.clone().unwrap_or_default(),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
let response = client
|
||||
.create_zone(Request::new(request))
|
||||
.await?
|
||||
.into_inner();
|
||||
let id = response.zone_id;
|
||||
|
||||
if self.wait || self.attach {
|
||||
wait_zone_started(&id, events.clone()).await?;
|
||||
}
|
||||
|
||||
let code = if self.attach {
|
||||
let input = StdioConsoleStream::stdin_stream(id.clone(), true).await;
|
||||
let output = client.attach_zone_console(input).await?.into_inner();
|
||||
let stdout_handle =
|
||||
tokio::task::spawn(async move { StdioConsoleStream::stdout(output, true).await });
|
||||
let exit_hook_task = StdioConsoleStream::zone_exit_hook(id.clone(), events).await?;
|
||||
select! {
|
||||
x = stdout_handle => {
|
||||
x??;
|
||||
None
|
||||
},
|
||||
x = exit_hook_task => x?
|
||||
}
|
||||
} else {
|
||||
println!("{}", id);
|
||||
None
|
||||
};
|
||||
StdioConsoleStream::restore_terminal_mode();
|
||||
std::process::exit(code.unwrap_or(0));
|
||||
}
|
||||
|
||||
async fn pull_image(
|
||||
&self,
|
||||
client: &mut ControlServiceClient<Channel>,
|
||||
image: &str,
|
||||
format: OciImageFormat,
|
||||
) -> Result<ZoneImageSpec> {
|
||||
let response = client
|
||||
.pull_image(PullImageRequest {
|
||||
image: image.to_string(),
|
||||
format: format.into(),
|
||||
overwrite_cache: self.pull_overwrite_cache,
|
||||
update: self.pull_update,
|
||||
})
|
||||
.await?;
|
||||
let reply = pull_interactive_progress(response.into_inner()).await?;
|
||||
Ok(ZoneImageSpec {
|
||||
image: Some(Image::Oci(ZoneOciImageSpec {
|
||||
digest: reply.digest,
|
||||
format: reply.format,
|
||||
})),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_zone_started(id: &str, events: EventStream) -> Result<()> {
|
||||
let mut stream = events.subscribe();
|
||||
while let Ok(event) = stream.recv().await {
|
||||
match event {
|
||||
Event::ZoneChanged(changed) => {
|
||||
let Some(zone) = changed.zone else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if zone.id != id {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(status) = zone.status else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(ref error) = status.error_status {
|
||||
if status.state() == ZoneState::Failed {
|
||||
error!("launch failed: {}", error.message);
|
||||
std::process::exit(1);
|
||||
} else {
|
||||
error!("zone error: {}", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if status.state() == ZoneState::Destroyed {
|
||||
error!("zone destroyed");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
if status.state() == ZoneState::Created {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn env_map(env: &[String]) -> HashMap<String, String> {
|
||||
let mut map = HashMap::<String, String>::new();
|
||||
for item in env {
|
||||
if let Some((key, value)) = item.split_once('=') {
|
||||
map.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
@ -1,181 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use clap::{Parser, ValueEnum};
|
||||
use comfy_table::{presets::UTF8_FULL_CONDENSED, Cell, Color, Table};
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::{
|
||||
common::Zone,
|
||||
control::{
|
||||
control_service_client::ControlServiceClient, ListZonesRequest, ResolveZoneIdRequest,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::format::{kv2line, proto2dynamic, proto2kv, zone_simple_line, zone_state_text};
|
||||
use krata::v1::common::ZoneState;
|
||||
use krata::v1::control::GetZoneRequest;
|
||||
use serde_json::Value;
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
|
||||
enum ZoneListFormat {
|
||||
Table,
|
||||
Json,
|
||||
JsonPretty,
|
||||
Jsonl,
|
||||
Yaml,
|
||||
KeyValue,
|
||||
Simple,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "List zone information")]
|
||||
pub struct ZoneListCommand {
|
||||
#[arg(short, long, default_value = "table", help = "Output format")]
|
||||
format: ZoneListFormat,
|
||||
#[arg(help = "Limit to a single zone, either the name or the uuid")]
|
||||
zone: Option<String>,
|
||||
}
|
||||
|
||||
impl ZoneListCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
_events: EventStream,
|
||||
) -> Result<()> {
|
||||
let mut zones = if let Some(ref zone) = self.zone {
|
||||
let reply = client
|
||||
.resolve_zone_id(Request::new(ResolveZoneIdRequest { name: zone.clone() }))
|
||||
.await?
|
||||
.into_inner();
|
||||
if !reply.zone_id.is_empty() {
|
||||
let reply = client
|
||||
.get_zone(Request::new(GetZoneRequest {
|
||||
zone_id: reply.zone_id,
|
||||
}))
|
||||
.await?
|
||||
.into_inner();
|
||||
if let Some(zone) = reply.zone {
|
||||
vec![zone]
|
||||
} else {
|
||||
return Err(anyhow!("unable to resolve zone '{}'", zone));
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow!("unable to resolve zone '{}'", zone));
|
||||
}
|
||||
} else {
|
||||
client
|
||||
.list_zones(Request::new(ListZonesRequest {}))
|
||||
.await?
|
||||
.into_inner()
|
||||
.zones
|
||||
};
|
||||
|
||||
zones.sort_by(|a, b| {
|
||||
a.spec
|
||||
.as_ref()
|
||||
.map(|x| x.name.as_str())
|
||||
.unwrap_or("")
|
||||
.cmp(b.spec.as_ref().map(|x| x.name.as_str()).unwrap_or(""))
|
||||
});
|
||||
|
||||
match self.format {
|
||||
ZoneListFormat::Table => {
|
||||
self.print_zone_table(zones)?;
|
||||
}
|
||||
|
||||
ZoneListFormat::Simple => {
|
||||
for zone in zones {
|
||||
println!("{}", zone_simple_line(&zone));
|
||||
}
|
||||
}
|
||||
|
||||
ZoneListFormat::Json | ZoneListFormat::JsonPretty | ZoneListFormat::Yaml => {
|
||||
let mut values = Vec::new();
|
||||
for zone in zones {
|
||||
let message = proto2dynamic(zone)?;
|
||||
values.push(serde_json::to_value(message)?);
|
||||
}
|
||||
let value = Value::Array(values);
|
||||
let encoded = if self.format == ZoneListFormat::JsonPretty {
|
||||
serde_json::to_string_pretty(&value)?
|
||||
} else if self.format == ZoneListFormat::Yaml {
|
||||
serde_yaml::to_string(&value)?
|
||||
} else {
|
||||
serde_json::to_string(&value)?
|
||||
};
|
||||
println!("{}", encoded.trim());
|
||||
}
|
||||
|
||||
ZoneListFormat::Jsonl => {
|
||||
for zone in zones {
|
||||
let message = proto2dynamic(zone)?;
|
||||
println!("{}", serde_json::to_string(&message)?);
|
||||
}
|
||||
}
|
||||
|
||||
ZoneListFormat::KeyValue => {
|
||||
self.print_key_value(zones)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_zone_table(&self, zones: Vec<Zone>) -> Result<()> {
|
||||
let mut table = Table::new();
|
||||
table.load_preset(UTF8_FULL_CONDENSED);
|
||||
table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
|
||||
table.set_header(vec!["name", "uuid", "state", "ipv4", "ipv6"]);
|
||||
for zone in zones {
|
||||
let ipv4 = zone
|
||||
.status
|
||||
.as_ref()
|
||||
.and_then(|x| x.network_status.as_ref())
|
||||
.map(|x| x.zone_ipv4.as_str())
|
||||
.unwrap_or("n/a");
|
||||
let ipv6 = zone
|
||||
.status
|
||||
.as_ref()
|
||||
.and_then(|x| x.network_status.as_ref())
|
||||
.map(|x| x.zone_ipv6.as_str())
|
||||
.unwrap_or("n/a");
|
||||
let Some(spec) = zone.spec else {
|
||||
continue;
|
||||
};
|
||||
let state = zone.status.as_ref().cloned().unwrap_or_default().state();
|
||||
let status_text = zone_state_text(state);
|
||||
|
||||
let status_color = match state {
|
||||
ZoneState::Destroyed | ZoneState::Failed => Color::Red,
|
||||
ZoneState::Destroying | ZoneState::Exited | ZoneState::Creating => Color::Yellow,
|
||||
ZoneState::Created => Color::Green,
|
||||
_ => Color::Reset,
|
||||
};
|
||||
|
||||
table.add_row(vec![
|
||||
Cell::new(spec.name),
|
||||
Cell::new(zone.id),
|
||||
Cell::new(status_text).fg(status_color),
|
||||
Cell::new(ipv4.to_string()),
|
||||
Cell::new(ipv6.to_string()),
|
||||
]);
|
||||
}
|
||||
if table.is_empty() {
|
||||
if self.zone.is_none() {
|
||||
println!("no zones have been launched");
|
||||
}
|
||||
} else {
|
||||
println!("{}", table);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_key_value(&self, zones: Vec<Zone>) -> Result<()> {
|
||||
for zone in zones {
|
||||
let kvs = proto2kv(zone)?;
|
||||
println!("{}", kv2line(kvs),);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use async_stream::stream;
|
||||
use clap::Parser;
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::control::{control_service_client::ControlServiceClient, ZoneConsoleRequest},
|
||||
};
|
||||
|
||||
use tokio::select;
|
||||
use tokio_stream::{pending, StreamExt};
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use crate::console::StdioConsoleStream;
|
||||
|
||||
use crate::cli::resolve_zone;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "View the logs of a zone")]
|
||||
pub struct ZoneLogsCommand {
|
||||
#[arg(short, long, help = "Follow output from the zone")]
|
||||
follow: bool,
|
||||
#[arg(help = "Zone to show logs for, either the name or the uuid")]
|
||||
zone: String,
|
||||
}
|
||||
|
||||
impl ZoneLogsCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
let zone_id: String = resolve_zone(&mut client, &self.zone).await?;
|
||||
let zone_id_stream = zone_id.clone();
|
||||
let follow = self.follow;
|
||||
let input = stream! {
|
||||
yield ZoneConsoleRequest { zone_id: zone_id_stream, replay_history: true, data: Vec::new() };
|
||||
if follow {
|
||||
let mut pending = pending::<ZoneConsoleRequest>();
|
||||
while let Some(x) = pending.next().await {
|
||||
yield x;
|
||||
}
|
||||
}
|
||||
};
|
||||
let output = client.attach_zone_console(input).await?.into_inner();
|
||||
let stdout_handle =
|
||||
tokio::task::spawn(async move { StdioConsoleStream::stdout(output, false).await });
|
||||
let exit_hook_task = StdioConsoleStream::zone_exit_hook(zone_id.clone(), events).await?;
|
||||
let code = select! {
|
||||
x = stdout_handle => {
|
||||
x??;
|
||||
None
|
||||
},
|
||||
x = exit_hook_task => x?
|
||||
};
|
||||
StdioConsoleStream::restore_terminal_mode();
|
||||
std::process::exit(code.unwrap_or(0));
|
||||
}
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::{
|
||||
common::ZoneMetricNode,
|
||||
control::{control_service_client::ControlServiceClient, ReadZoneMetricsRequest},
|
||||
},
|
||||
};
|
||||
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use crate::format::{kv2line, metrics_flat, metrics_tree, proto2dynamic};
|
||||
|
||||
use crate::cli::resolve_zone;
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
|
||||
enum ZoneMetricsFormat {
|
||||
Tree,
|
||||
Json,
|
||||
JsonPretty,
|
||||
Yaml,
|
||||
KeyValue,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Read metrics from the zone")]
|
||||
pub struct ZoneMetricsCommand {
|
||||
#[arg(short, long, default_value = "tree", help = "Output format")]
|
||||
format: ZoneMetricsFormat,
|
||||
#[arg(help = "Zone to read metrics for, either the name or the uuid")]
|
||||
zone: String,
|
||||
}
|
||||
|
||||
impl ZoneMetricsCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
_events: EventStream,
|
||||
) -> Result<()> {
|
||||
let zone_id: String = resolve_zone(&mut client, &self.zone).await?;
|
||||
let root = client
|
||||
.read_zone_metrics(ReadZoneMetricsRequest { zone_id })
|
||||
.await?
|
||||
.into_inner()
|
||||
.root
|
||||
.unwrap_or_default();
|
||||
match self.format {
|
||||
ZoneMetricsFormat::Tree => {
|
||||
self.print_metrics_tree(root)?;
|
||||
}
|
||||
|
||||
ZoneMetricsFormat::Json | ZoneMetricsFormat::JsonPretty | ZoneMetricsFormat::Yaml => {
|
||||
let value = serde_json::to_value(proto2dynamic(root)?)?;
|
||||
let encoded = if self.format == ZoneMetricsFormat::JsonPretty {
|
||||
serde_json::to_string_pretty(&value)?
|
||||
} else if self.format == ZoneMetricsFormat::Yaml {
|
||||
serde_yaml::to_string(&value)?
|
||||
} else {
|
||||
serde_json::to_string(&value)?
|
||||
};
|
||||
println!("{}", encoded.trim());
|
||||
}
|
||||
|
||||
ZoneMetricsFormat::KeyValue => {
|
||||
self.print_key_value(root)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_metrics_tree(&self, root: ZoneMetricNode) -> Result<()> {
|
||||
print!("{}", metrics_tree(root));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_key_value(&self, metrics: ZoneMetricNode) -> Result<()> {
|
||||
let kvs = metrics_flat(metrics);
|
||||
println!("{}", kv2line(kvs));
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use krata::events::EventStream;
|
||||
use krata::v1::control::control_service_client::ControlServiceClient;
|
||||
|
||||
use crate::cli::zone::attach::ZoneAttachCommand;
|
||||
use crate::cli::zone::destroy::ZoneDestroyCommand;
|
||||
use crate::cli::zone::exec::ZoneExecCommand;
|
||||
use crate::cli::zone::launch::ZoneLaunchCommand;
|
||||
use crate::cli::zone::list::ZoneListCommand;
|
||||
use crate::cli::zone::logs::ZoneLogsCommand;
|
||||
use crate::cli::zone::metrics::ZoneMetricsCommand;
|
||||
use crate::cli::zone::resolve::ZoneResolveCommand;
|
||||
use crate::cli::zone::top::ZoneTopCommand;
|
||||
use crate::cli::zone::update_resources::ZoneUpdateResourcesCommand;
|
||||
use crate::cli::zone::watch::ZoneWatchCommand;
|
||||
|
||||
pub mod attach;
|
||||
pub mod destroy;
|
||||
pub mod exec;
|
||||
pub mod launch;
|
||||
pub mod list;
|
||||
pub mod logs;
|
||||
pub mod metrics;
|
||||
pub mod resolve;
|
||||
pub mod top;
|
||||
pub mod update_resources;
|
||||
pub mod watch;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Manage the zones on the isolation engine")]
|
||||
pub struct ZoneCommand {
|
||||
#[command(subcommand)]
|
||||
subcommand: ZoneCommands,
|
||||
}
|
||||
|
||||
impl ZoneCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
self.subcommand.run(client, events).await
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Subcommand)]
|
||||
pub enum ZoneCommands {
|
||||
Attach(ZoneAttachCommand),
|
||||
List(ZoneListCommand),
|
||||
Launch(ZoneLaunchCommand),
|
||||
Destroy(ZoneDestroyCommand),
|
||||
Exec(ZoneExecCommand),
|
||||
Logs(ZoneLogsCommand),
|
||||
Metrics(ZoneMetricsCommand),
|
||||
Resolve(ZoneResolveCommand),
|
||||
Top(ZoneTopCommand),
|
||||
Watch(ZoneWatchCommand),
|
||||
UpdateResources(ZoneUpdateResourcesCommand),
|
||||
}
|
||||
|
||||
impl ZoneCommands {
|
||||
pub async fn run(
|
||||
self,
|
||||
client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
match self {
|
||||
ZoneCommands::Launch(launch) => launch.run(client, events).await,
|
||||
|
||||
ZoneCommands::Destroy(destroy) => destroy.run(client, events).await,
|
||||
|
||||
ZoneCommands::Attach(attach) => attach.run(client, events).await,
|
||||
|
||||
ZoneCommands::Logs(logs) => logs.run(client, events).await,
|
||||
|
||||
ZoneCommands::List(list) => list.run(client, events).await,
|
||||
|
||||
ZoneCommands::Watch(watch) => watch.run(events).await,
|
||||
|
||||
ZoneCommands::Resolve(resolve) => resolve.run(client).await,
|
||||
|
||||
ZoneCommands::Metrics(metrics) => metrics.run(client, events).await,
|
||||
|
||||
ZoneCommands::Top(top) => top.run(client, events).await,
|
||||
|
||||
ZoneCommands::Exec(exec) => exec.run(client).await,
|
||||
|
||||
ZoneCommands::UpdateResources(update_resources) => update_resources.run(client).await,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use krata::v1::control::{control_service_client::ControlServiceClient, ResolveZoneIdRequest};
|
||||
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Resolve a zone name to a uuid")]
|
||||
pub struct ZoneResolveCommand {
|
||||
#[arg(help = "Zone name")]
|
||||
zone: String,
|
||||
}
|
||||
|
||||
impl ZoneResolveCommand {
|
||||
pub async fn run(self, mut client: ControlServiceClient<Channel>) -> Result<()> {
|
||||
let reply = client
|
||||
.resolve_zone_id(Request::new(ResolveZoneIdRequest {
|
||||
name: self.zone.clone(),
|
||||
}))
|
||||
.await?
|
||||
.into_inner();
|
||||
if !reply.zone_id.is_empty() {
|
||||
println!("{}", reply.zone_id);
|
||||
} else {
|
||||
std::process::exit(1);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,215 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use krata::{events::EventStream, v1::control::control_service_client::ControlServiceClient};
|
||||
use std::{
|
||||
io::{self, stdout, Stdout},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::select;
|
||||
use tokio_stream::StreamExt;
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use crossterm::{
|
||||
event::{Event, KeyCode, KeyEvent, KeyEventKind},
|
||||
execute,
|
||||
terminal::*,
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
symbols::border,
|
||||
widgets::{
|
||||
block::{Position, Title},
|
||||
Block, Borders, Row, Table, TableState,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
format::zone_state_text,
|
||||
metrics::{
|
||||
lookup_metric_value, MultiMetricCollector, MultiMetricCollectorHandle, MultiMetricState,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Dashboard for running zones")]
|
||||
pub struct ZoneTopCommand {}
|
||||
|
||||
pub type Tui = Terminal<CrosstermBackend<Stdout>>;
|
||||
|
||||
impl ZoneTopCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
let collector = MultiMetricCollector::new(client, events, Duration::from_millis(200))?;
|
||||
let collector = collector.launch().await?;
|
||||
let mut tui = ZoneTopCommand::init()?;
|
||||
let mut app = ZoneTopApp {
|
||||
metrics: MultiMetricState { zones: vec![] },
|
||||
exit: false,
|
||||
table: TableState::new(),
|
||||
};
|
||||
app.run(collector, &mut tui).await?;
|
||||
ZoneTopCommand::restore()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn init() -> io::Result<Tui> {
|
||||
execute!(stdout(), EnterAlternateScreen)?;
|
||||
enable_raw_mode()?;
|
||||
Terminal::new(CrosstermBackend::new(stdout()))
|
||||
}
|
||||
|
||||
pub fn restore() -> io::Result<()> {
|
||||
execute!(stdout(), LeaveAlternateScreen)?;
|
||||
disable_raw_mode()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ZoneTopApp {
|
||||
table: TableState,
|
||||
metrics: MultiMetricState,
|
||||
exit: bool,
|
||||
}
|
||||
|
||||
impl ZoneTopApp {
|
||||
pub async fn run(
|
||||
&mut self,
|
||||
mut collector: MultiMetricCollectorHandle,
|
||||
terminal: &mut Tui,
|
||||
) -> Result<()> {
|
||||
let mut events = crossterm::event::EventStream::new();
|
||||
|
||||
while !self.exit {
|
||||
terminal.draw(|frame| self.render_frame(frame))?;
|
||||
|
||||
select! {
|
||||
x = collector.receiver.recv() => match x {
|
||||
Some(state) => {
|
||||
self.metrics = state;
|
||||
},
|
||||
|
||||
None => {
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
x = events.next() => match x {
|
||||
Some(event) => {
|
||||
let event = event?;
|
||||
self.handle_event(event)?;
|
||||
},
|
||||
|
||||
None => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_frame(&mut self, frame: &mut Frame) {
|
||||
frame.render_widget(self, frame.area());
|
||||
}
|
||||
|
||||
fn handle_event(&mut self, event: Event) -> io::Result<()> {
|
||||
match event {
|
||||
Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
|
||||
self.handle_key_event(key_event)
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn exit(&mut self) {
|
||||
self.exit = true;
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
||||
if let KeyCode::Char('q') = key_event.code {
|
||||
self.exit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &mut ZoneTopApp {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let title = Title::from(" krata isolation engine ".bold());
|
||||
let instructions = Title::from(vec![" Quit ".into(), "<Q> ".blue().bold()]);
|
||||
let block = Block::default()
|
||||
.title(title.alignment(Alignment::Center))
|
||||
.title(
|
||||
instructions
|
||||
.alignment(Alignment::Center)
|
||||
.position(Position::Bottom),
|
||||
)
|
||||
.borders(Borders::ALL)
|
||||
.border_set(border::THICK);
|
||||
|
||||
let mut rows = vec![];
|
||||
|
||||
for ms in &self.metrics.zones {
|
||||
let Some(ref spec) = ms.zone.spec else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(ref status) = ms.zone.status else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let memory_total = ms
|
||||
.root
|
||||
.as_ref()
|
||||
.and_then(|root| lookup_metric_value(root, "system/memory/total"));
|
||||
let memory_used = ms
|
||||
.root
|
||||
.as_ref()
|
||||
.and_then(|root| lookup_metric_value(root, "system/memory/used"));
|
||||
let memory_free = ms
|
||||
.root
|
||||
.as_ref()
|
||||
.and_then(|root| lookup_metric_value(root, "system/memory/free"));
|
||||
|
||||
let row = Row::new(vec![
|
||||
spec.name.clone(),
|
||||
ms.zone.id.clone(),
|
||||
zone_state_text(status.state()),
|
||||
memory_total.unwrap_or_default(),
|
||||
memory_used.unwrap_or_default(),
|
||||
memory_free.unwrap_or_default(),
|
||||
]);
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
let widths = [
|
||||
Constraint::Min(8),
|
||||
Constraint::Min(8),
|
||||
Constraint::Min(8),
|
||||
Constraint::Min(8),
|
||||
Constraint::Min(8),
|
||||
Constraint::Min(8),
|
||||
];
|
||||
|
||||
let table = Table::new(rows, widths)
|
||||
.header(
|
||||
Row::new(vec![
|
||||
"name",
|
||||
"id",
|
||||
"status",
|
||||
"total memory",
|
||||
"used memory",
|
||||
"free memory",
|
||||
])
|
||||
.style(Style::new().bold())
|
||||
.bottom_margin(1),
|
||||
)
|
||||
.column_spacing(1)
|
||||
.block(block);
|
||||
|
||||
StatefulWidget::render(table, area, buf, &mut self.table);
|
||||
}
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use krata::v1::{
|
||||
common::ZoneResourceSpec,
|
||||
control::{control_service_client::ControlServiceClient, UpdateZoneResourcesRequest},
|
||||
};
|
||||
|
||||
use crate::cli::resolve_zone;
|
||||
use krata::v1::control::GetZoneRequest;
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Update the available resources to a zone")]
|
||||
pub struct ZoneUpdateResourcesCommand {
|
||||
#[arg(help = "Zone to update resources of, either the name or the uuid")]
|
||||
zone: String,
|
||||
#[arg(
|
||||
short = 'C',
|
||||
long = "max-cpus",
|
||||
default_value_t = 0,
|
||||
help = "Maximum vCPUs available to the zone (0 means previous value)"
|
||||
)]
|
||||
max_cpus: u32,
|
||||
#[arg(
|
||||
short = 'c',
|
||||
long = "target-cpus",
|
||||
default_value_t = 0,
|
||||
help = "Target vCPUs for the zone to use (0 means previous value)"
|
||||
)]
|
||||
target_cpus: u32,
|
||||
#[arg(
|
||||
short = 'M',
|
||||
long = "max-memory",
|
||||
default_value_t = 0,
|
||||
help = "Maximum memory available to the zone, in megabytes (0 means previous value)"
|
||||
)]
|
||||
max_memory: u64,
|
||||
#[arg(
|
||||
short = 'm',
|
||||
long = "target-memory",
|
||||
default_value_t = 0,
|
||||
help = "Target memory for the zone to use, in megabytes (0 means previous value)"
|
||||
)]
|
||||
target_memory: u64,
|
||||
}
|
||||
|
||||
impl ZoneUpdateResourcesCommand {
|
||||
pub async fn run(self, mut client: ControlServiceClient<Channel>) -> Result<()> {
|
||||
let zone_id = resolve_zone(&mut client, &self.zone).await?;
|
||||
let zone = client
|
||||
.get_zone(GetZoneRequest { zone_id })
|
||||
.await?
|
||||
.into_inner()
|
||||
.zone
|
||||
.unwrap_or_default();
|
||||
let active_resources = zone
|
||||
.status
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.resource_status
|
||||
.unwrap_or_default()
|
||||
.active_resources
|
||||
.unwrap_or_default();
|
||||
client
|
||||
.update_zone_resources(Request::new(UpdateZoneResourcesRequest {
|
||||
zone_id: zone.id.clone(),
|
||||
resources: Some(ZoneResourceSpec {
|
||||
max_memory: if self.max_memory == 0 {
|
||||
active_resources.max_memory
|
||||
} else {
|
||||
self.max_memory
|
||||
},
|
||||
target_memory: if self.target_memory == 0 {
|
||||
active_resources.target_memory
|
||||
} else {
|
||||
self.target_memory
|
||||
},
|
||||
max_cpus: if self.max_cpus == 0 {
|
||||
active_resources.max_cpus
|
||||
} else {
|
||||
self.max_cpus
|
||||
},
|
||||
target_cpus: if self.target_cpus == 0 {
|
||||
active_resources.target_cpus
|
||||
} else {
|
||||
self.target_cpus
|
||||
},
|
||||
}),
|
||||
}))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::{common::Zone, control::watch_events_reply::Event},
|
||||
};
|
||||
use prost_reflect::ReflectMessage;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::format::{kv2line, proto2dynamic, proto2kv, zone_simple_line};
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
|
||||
enum ZoneWatchFormat {
|
||||
Simple,
|
||||
Json,
|
||||
KeyValue,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Watch for zone changes")]
|
||||
pub struct ZoneWatchCommand {
|
||||
#[arg(short, long, default_value = "simple", help = "Output format")]
|
||||
format: ZoneWatchFormat,
|
||||
}
|
||||
|
||||
impl ZoneWatchCommand {
|
||||
pub async fn run(self, events: EventStream) -> Result<()> {
|
||||
let mut stream = events.subscribe();
|
||||
loop {
|
||||
let event = stream.recv().await?;
|
||||
|
||||
let Event::ZoneChanged(changed) = event;
|
||||
let zone = changed.zone.clone();
|
||||
self.print_event("zone.changed", changed, zone)?;
|
||||
}
|
||||
}
|
||||
|
||||
fn print_event(&self, typ: &str, event: impl ReflectMessage, zone: Option<Zone>) -> Result<()> {
|
||||
match self.format {
|
||||
ZoneWatchFormat::Simple => {
|
||||
if let Some(zone) = zone {
|
||||
println!("{}", zone_simple_line(&zone));
|
||||
}
|
||||
}
|
||||
|
||||
ZoneWatchFormat::Json => {
|
||||
let message = proto2dynamic(event)?;
|
||||
let mut value = serde_json::to_value(&message)?;
|
||||
if let Value::Object(ref mut map) = value {
|
||||
map.insert("event.type".to_string(), Value::String(typ.to_string()));
|
||||
}
|
||||
println!("{}", serde_json::to_string(&value)?);
|
||||
}
|
||||
|
||||
ZoneWatchFormat::KeyValue => {
|
||||
let mut map = proto2kv(event)?;
|
||||
map.insert("event.type".to_string(), typ.to_string());
|
||||
println!("{}", kv2line(map),);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,263 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use async_stream::stream;
|
||||
use crossterm::{
|
||||
terminal::{disable_raw_mode, enable_raw_mode, is_raw_mode_enabled},
|
||||
tty::IsTty,
|
||||
};
|
||||
use krata::v1::common::ZoneState;
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::common::TerminalSize,
|
||||
v1::control::{
|
||||
watch_events_reply::Event, ExecInsideZoneReply, ExecInsideZoneRequest, ZoneConsoleReply,
|
||||
ZoneConsoleRequest,
|
||||
},
|
||||
};
|
||||
use log::debug;
|
||||
use tokio::{
|
||||
io::{stderr, stdin, stdout, AsyncReadExt, AsyncWriteExt},
|
||||
select,
|
||||
task::JoinHandle,
|
||||
};
|
||||
use tokio_stream::{Stream, StreamExt};
|
||||
use tonic::Streaming;
|
||||
|
||||
pub struct StdioConsoleStream;
|
||||
|
||||
enum ExecStdinSelect {
|
||||
DataRead(std::io::Result<usize>),
|
||||
TerminalResize,
|
||||
}
|
||||
|
||||
impl StdioConsoleStream {
|
||||
pub async fn stdin_stream(
|
||||
zone: String,
|
||||
replay_history: bool,
|
||||
) -> impl Stream<Item = ZoneConsoleRequest> {
|
||||
let mut stdin = stdin();
|
||||
stream! {
|
||||
yield ZoneConsoleRequest { zone_id: zone, replay_history, data: vec![] };
|
||||
|
||||
let mut buffer = vec![0u8; 60];
|
||||
loop {
|
||||
let size = match stdin.read(&mut buffer).await {
|
||||
Ok(size) => size,
|
||||
Err(error) => {
|
||||
debug!("failed to read stdin: {}", error);
|
||||
break;
|
||||
}
|
||||
};
|
||||
let data = buffer[0..size].to_vec();
|
||||
if size == 1 && buffer[0] == 0x1d {
|
||||
break;
|
||||
}
|
||||
yield ZoneConsoleRequest { zone_id: String::default(), replay_history, data };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub async fn input_stream_exec(
|
||||
initial: ExecInsideZoneRequest,
|
||||
tty: bool,
|
||||
) -> impl Stream<Item = ExecInsideZoneRequest> {
|
||||
let mut stdin = stdin();
|
||||
stream! {
|
||||
yield initial;
|
||||
|
||||
let mut buffer = vec![0u8; 60];
|
||||
let mut terminal_size_change = if tty {
|
||||
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::window_change()).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut stdin_closed = false;
|
||||
loop {
|
||||
let selected = if let Some(ref mut terminal_size_change) = terminal_size_change {
|
||||
if stdin_closed {
|
||||
select! {
|
||||
_ = terminal_size_change.recv() => ExecStdinSelect::TerminalResize,
|
||||
}
|
||||
} else {
|
||||
select! {
|
||||
result = stdin.read(&mut buffer) => ExecStdinSelect::DataRead(result),
|
||||
_ = terminal_size_change.recv() => ExecStdinSelect::TerminalResize,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
select! {
|
||||
result = stdin.read(&mut buffer) => ExecStdinSelect::DataRead(result),
|
||||
}
|
||||
};
|
||||
|
||||
match selected {
|
||||
ExecStdinSelect::DataRead(result) => {
|
||||
match result {
|
||||
Ok(size) => {
|
||||
let stdin = buffer[0..size].to_vec();
|
||||
if size == 1 && buffer[0] == 0x1d {
|
||||
break;
|
||||
}
|
||||
stdin_closed = size == 0;
|
||||
yield ExecInsideZoneRequest { zone_id: String::default(), task: None, terminal_size: None, stdin, stdin_closed, };
|
||||
},
|
||||
Err(error) => {
|
||||
debug!("failed to read stdin: {}", error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
ExecStdinSelect::TerminalResize => {
|
||||
if let Ok((columns, rows)) = crossterm::terminal::size() {
|
||||
yield ExecInsideZoneRequest { zone_id: String::default(), task: None, terminal_size: Some(TerminalSize {
|
||||
rows: rows as u32,
|
||||
columns: columns as u32,
|
||||
}), stdin: vec![], stdin_closed: false, };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
pub async fn input_stream_exec(
|
||||
initial: ExecInsideZoneRequest,
|
||||
_tty: bool,
|
||||
) -> impl Stream<Item = ExecInsideZoneRequest> {
|
||||
let mut stdin = stdin();
|
||||
stream! {
|
||||
yield initial;
|
||||
|
||||
let mut buffer = vec![0u8; 60];
|
||||
let mut stdin_closed = false;
|
||||
loop {
|
||||
let selected = select! {
|
||||
result = stdin.read(&mut buffer) => ExecStdinSelect::DataRead(result),
|
||||
};
|
||||
|
||||
match selected {
|
||||
ExecStdinSelect::DataRead(result) => {
|
||||
match result {
|
||||
Ok(size) => {
|
||||
let stdin = buffer[0..size].to_vec();
|
||||
if size == 1 && buffer[0] == 0x1d {
|
||||
break;
|
||||
}
|
||||
stdin_closed = size == 0;
|
||||
yield ExecInsideZoneRequest { zone_id: String::default(), task: None, terminal_size: None, stdin, stdin_closed, };
|
||||
},
|
||||
Err(error) => {
|
||||
debug!("failed to read stdin: {}", error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn stdout(mut stream: Streaming<ZoneConsoleReply>, raw: bool) -> Result<()> {
|
||||
if raw && stdin().is_tty() {
|
||||
enable_raw_mode()?;
|
||||
StdioConsoleStream::register_terminal_restore_hook()?;
|
||||
}
|
||||
let mut stdout = stdout();
|
||||
while let Some(reply) = stream.next().await {
|
||||
let reply = reply?;
|
||||
if reply.data.is_empty() {
|
||||
continue;
|
||||
}
|
||||
stdout.write_all(&reply.data).await?;
|
||||
stdout.flush().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn exec_output(mut stream: Streaming<ExecInsideZoneReply>, raw: bool) -> Result<i32> {
|
||||
if raw {
|
||||
enable_raw_mode()?;
|
||||
StdioConsoleStream::register_terminal_restore_hook()?;
|
||||
}
|
||||
let mut stdout = stdout();
|
||||
let mut stderr = stderr();
|
||||
while let Some(reply) = stream.next().await {
|
||||
let reply = reply?;
|
||||
if !reply.stdout.is_empty() {
|
||||
stdout.write_all(&reply.stdout).await?;
|
||||
stdout.flush().await?;
|
||||
}
|
||||
|
||||
if !reply.stderr.is_empty() {
|
||||
stderr.write_all(&reply.stderr).await?;
|
||||
stderr.flush().await?;
|
||||
}
|
||||
|
||||
if reply.exited {
|
||||
return if reply.error.is_empty() {
|
||||
Ok(reply.exit_code)
|
||||
} else {
|
||||
StdioConsoleStream::restore_terminal_mode();
|
||||
stderr
|
||||
.write_all(format!("Error: exec failed: {}\n", reply.error).as_bytes())
|
||||
.await?;
|
||||
stderr.flush().await?;
|
||||
Ok(-1)
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(-1)
|
||||
}
|
||||
|
||||
pub async fn zone_exit_hook(
|
||||
id: String,
|
||||
events: EventStream,
|
||||
) -> Result<JoinHandle<Option<i32>>> {
|
||||
Ok(tokio::task::spawn(async move {
|
||||
let mut stream = events.subscribe();
|
||||
while let Ok(event) = stream.recv().await {
|
||||
let Event::ZoneChanged(changed) = event;
|
||||
let Some(zone) = changed.zone else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(status) = zone.status else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if zone.id != id {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(exit_status) = status.exit_status {
|
||||
return Some(exit_status.code);
|
||||
}
|
||||
|
||||
let state = status.state();
|
||||
if state == ZoneState::Destroying || state == ZoneState::Destroyed {
|
||||
return Some(10);
|
||||
}
|
||||
}
|
||||
None
|
||||
}))
|
||||
}
|
||||
|
||||
fn register_terminal_restore_hook() -> Result<()> {
|
||||
if stdin().is_tty() {
|
||||
ctrlc::set_handler(move || {
|
||||
StdioConsoleStream::restore_terminal_mode();
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn restore_terminal_mode() {
|
||||
if is_raw_mode_enabled().unwrap_or(false) {
|
||||
let _ = disable_raw_mode();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,164 +0,0 @@
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
|
||||
use anyhow::Result;
|
||||
use fancy_duration::FancyDuration;
|
||||
use human_bytes::human_bytes;
|
||||
use prost_reflect::{DynamicMessage, ReflectMessage};
|
||||
use prost_types::Value;
|
||||
use termtree::Tree;
|
||||
|
||||
use krata::v1::common::{Zone, ZoneMetricFormat, ZoneMetricNode, ZoneState};
|
||||
|
||||
pub fn proto2dynamic(proto: impl ReflectMessage) -> Result<DynamicMessage> {
|
||||
Ok(DynamicMessage::decode(
|
||||
proto.descriptor(),
|
||||
proto.encode_to_vec().as_slice(),
|
||||
)?)
|
||||
}
|
||||
|
||||
pub fn value2kv(value: serde_json::Value) -> Result<HashMap<String, String>> {
|
||||
let mut map = HashMap::new();
|
||||
fn crawl(prefix: String, map: &mut HashMap<String, String>, value: serde_json::Value) {
|
||||
fn dot(prefix: &str, next: String) -> String {
|
||||
if prefix.is_empty() {
|
||||
next.to_string()
|
||||
} else {
|
||||
format!("{}.{}", prefix, next)
|
||||
}
|
||||
}
|
||||
|
||||
match value {
|
||||
serde_json::Value::Null => {
|
||||
map.insert(prefix, "null".to_string());
|
||||
}
|
||||
|
||||
serde_json::Value::String(value) => {
|
||||
map.insert(prefix, value);
|
||||
}
|
||||
|
||||
serde_json::Value::Bool(value) => {
|
||||
map.insert(prefix, value.to_string());
|
||||
}
|
||||
|
||||
serde_json::Value::Number(value) => {
|
||||
map.insert(prefix, value.to_string());
|
||||
}
|
||||
|
||||
serde_json::Value::Array(value) => {
|
||||
for (i, item) in value.into_iter().enumerate() {
|
||||
let next = dot(&prefix, i.to_string());
|
||||
crawl(next, map, item);
|
||||
}
|
||||
}
|
||||
|
||||
serde_json::Value::Object(value) => {
|
||||
for (key, item) in value {
|
||||
let next = dot(&prefix, key);
|
||||
crawl(next, map, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
crawl("".to_string(), &mut map, value);
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
pub fn proto2kv(proto: impl ReflectMessage) -> Result<HashMap<String, String>> {
|
||||
let message = proto2dynamic(proto)?;
|
||||
let value = serde_json::to_value(message)?;
|
||||
value2kv(value)
|
||||
}
|
||||
|
||||
pub fn kv2line(map: HashMap<String, String>) -> String {
|
||||
map.iter()
|
||||
.map(|(k, v)| format!("{}=\"{}\"", k, v.replace('"', "\\\"")))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
pub fn zone_state_text(status: ZoneState) -> String {
|
||||
match status {
|
||||
ZoneState::Creating => "creating",
|
||||
ZoneState::Created => "created",
|
||||
ZoneState::Destroying => "destroying",
|
||||
ZoneState::Destroyed => "destroyed",
|
||||
ZoneState::Exited => "exited",
|
||||
ZoneState::Failed => "failed",
|
||||
_ => "unknown",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub fn zone_simple_line(zone: &Zone) -> String {
|
||||
let state = zone_state_text(
|
||||
zone.status
|
||||
.as_ref()
|
||||
.map(|x| x.state())
|
||||
.unwrap_or(ZoneState::Unknown),
|
||||
);
|
||||
let name = zone.spec.as_ref().map(|x| x.name.as_str()).unwrap_or("");
|
||||
let network_status = zone.status.as_ref().and_then(|x| x.network_status.as_ref());
|
||||
let ipv4 = network_status.map(|x| x.zone_ipv4.as_str()).unwrap_or("");
|
||||
let ipv6 = network_status.map(|x| x.zone_ipv6.as_str()).unwrap_or("");
|
||||
format!("{}\t{}\t{}\t{}\t{}", zone.id, state, name, ipv4, ipv6)
|
||||
}
|
||||
|
||||
fn metrics_value_string(value: Value) -> String {
|
||||
proto2dynamic(value)
|
||||
.map(|x| serde_json::to_string(&x).ok())
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn metrics_value_numeric(value: Value) -> f64 {
|
||||
let string = metrics_value_string(value);
|
||||
string.parse::<f64>().ok().unwrap_or(f64::NAN)
|
||||
}
|
||||
|
||||
pub fn metrics_value_pretty(value: Value, format: ZoneMetricFormat) -> String {
|
||||
match format {
|
||||
ZoneMetricFormat::Bytes => human_bytes(metrics_value_numeric(value)),
|
||||
ZoneMetricFormat::Integer => (metrics_value_numeric(value) as u64).to_string(),
|
||||
ZoneMetricFormat::DurationSeconds => {
|
||||
FancyDuration(Duration::from_secs_f64(metrics_value_numeric(value))).to_string()
|
||||
}
|
||||
_ => metrics_value_string(value),
|
||||
}
|
||||
}
|
||||
|
||||
fn metrics_flat_internal(prefix: &str, node: ZoneMetricNode, map: &mut HashMap<String, String>) {
|
||||
if let Some(value) = node.value {
|
||||
map.insert(prefix.to_string(), metrics_value_string(value));
|
||||
}
|
||||
|
||||
for child in node.children {
|
||||
let path = if prefix.is_empty() {
|
||||
child.name.to_string()
|
||||
} else {
|
||||
format!("{}.{}", prefix, child.name)
|
||||
};
|
||||
metrics_flat_internal(&path, child, map);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn metrics_flat(root: ZoneMetricNode) -> HashMap<String, String> {
|
||||
let mut map = HashMap::new();
|
||||
metrics_flat_internal("", root, &mut map);
|
||||
map
|
||||
}
|
||||
|
||||
pub fn metrics_tree(node: ZoneMetricNode) -> Tree<String> {
|
||||
let mut name = node.name.to_string();
|
||||
let format = node.format();
|
||||
if let Some(value) = node.value {
|
||||
let value_string = metrics_value_pretty(value, format);
|
||||
name.push_str(&format!(": {}", value_string));
|
||||
}
|
||||
|
||||
let mut tree = Tree::new(name);
|
||||
for child in node.children {
|
||||
tree.push(metrics_tree(child));
|
||||
}
|
||||
tree
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
pub mod cli;
|
||||
pub mod console;
|
||||
pub mod format;
|
||||
pub mod metrics;
|
||||
pub mod pull;
|
@ -1,158 +0,0 @@
|
||||
use crate::format::metrics_value_pretty;
|
||||
use anyhow::Result;
|
||||
use krata::v1::common::ZoneState;
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::{
|
||||
common::{Zone, ZoneMetricNode},
|
||||
control::{
|
||||
control_service_client::ControlServiceClient, watch_events_reply::Event,
|
||||
ListZonesRequest, ReadZoneMetricsRequest,
|
||||
},
|
||||
},
|
||||
};
|
||||
use log::error;
|
||||
use std::time::Duration;
|
||||
use tokio::{
|
||||
select,
|
||||
sync::mpsc::{channel, Receiver, Sender},
|
||||
task::JoinHandle,
|
||||
time::{sleep, timeout},
|
||||
};
|
||||
use tonic::transport::Channel;
|
||||
|
||||
pub struct MetricState {
|
||||
pub zone: Zone,
|
||||
pub root: Option<ZoneMetricNode>,
|
||||
}
|
||||
|
||||
pub struct MultiMetricState {
|
||||
pub zones: Vec<MetricState>,
|
||||
}
|
||||
|
||||
pub struct MultiMetricCollector {
|
||||
client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
period: Duration,
|
||||
}
|
||||
|
||||
pub struct MultiMetricCollectorHandle {
|
||||
pub receiver: Receiver<MultiMetricState>,
|
||||
task: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl Drop for MultiMetricCollectorHandle {
|
||||
fn drop(&mut self) {
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
||||
|
||||
impl MultiMetricCollector {
|
||||
pub fn new(
|
||||
client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
period: Duration,
|
||||
) -> Result<MultiMetricCollector> {
|
||||
Ok(MultiMetricCollector {
|
||||
client,
|
||||
events,
|
||||
period,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn launch(mut self) -> Result<MultiMetricCollectorHandle> {
|
||||
let (sender, receiver) = channel::<MultiMetricState>(100);
|
||||
let task = tokio::task::spawn(async move {
|
||||
if let Err(error) = self.process(sender).await {
|
||||
error!("failed to process multi metric collector: {}", error);
|
||||
}
|
||||
});
|
||||
Ok(MultiMetricCollectorHandle { receiver, task })
|
||||
}
|
||||
|
||||
pub async fn process(&mut self, sender: Sender<MultiMetricState>) -> Result<()> {
|
||||
let mut events = self.events.subscribe();
|
||||
let mut zones: Vec<Zone> = self
|
||||
.client
|
||||
.list_zones(ListZonesRequest {})
|
||||
.await?
|
||||
.into_inner()
|
||||
.zones;
|
||||
loop {
|
||||
let collect = select! {
|
||||
x = events.recv() => match x {
|
||||
Ok(event) => {
|
||||
let Event::ZoneChanged(changed) = event;
|
||||
let Some(zone) = changed.zone else {
|
||||
continue;
|
||||
};
|
||||
let Some(ref status) = zone.status else {
|
||||
continue;
|
||||
};
|
||||
zones.retain(|x| x.id != zone.id);
|
||||
if status.state() != ZoneState::Destroying {
|
||||
zones.push(zone);
|
||||
}
|
||||
false
|
||||
},
|
||||
|
||||
Err(error) => {
|
||||
return Err(error.into());
|
||||
}
|
||||
},
|
||||
|
||||
_ = sleep(self.period) => {
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
if !collect {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut metrics = Vec::new();
|
||||
for zone in &zones {
|
||||
let Some(ref status) = zone.status else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if status.state() != ZoneState::Created {
|
||||
continue;
|
||||
}
|
||||
|
||||
let root = timeout(
|
||||
Duration::from_secs(5),
|
||||
self.client.read_zone_metrics(ReadZoneMetricsRequest {
|
||||
zone_id: zone.id.clone(),
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|x| x.ok())
|
||||
.map(|x| x.into_inner())
|
||||
.and_then(|x| x.root);
|
||||
metrics.push(MetricState {
|
||||
zone: zone.clone(),
|
||||
root,
|
||||
});
|
||||
}
|
||||
sender.send(MultiMetricState { zones: metrics }).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lookup<'a>(node: &'a ZoneMetricNode, path: &str) -> Option<&'a ZoneMetricNode> {
|
||||
let Some((what, b)) = path.split_once('/') else {
|
||||
return node.children.iter().find(|x| x.name == path);
|
||||
};
|
||||
let next = node.children.iter().find(|x| x.name == what)?;
|
||||
return lookup(next, b);
|
||||
}
|
||||
|
||||
pub fn lookup_metric_value(node: &ZoneMetricNode, path: &str) -> Option<String> {
|
||||
lookup(node, path).and_then(|x| {
|
||||
x.value
|
||||
.as_ref()
|
||||
.map(|v| metrics_value_pretty(v.clone(), x.format()))
|
||||
})
|
||||
}
|
@ -1,268 +0,0 @@
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashMap},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
||||
use krata::v1::control::{
|
||||
image_progress_indication::Indication, ImageProgressIndication, ImageProgressLayerPhase,
|
||||
ImageProgressPhase, PullImageReply,
|
||||
};
|
||||
use tokio_stream::StreamExt;
|
||||
use tonic::Streaming;
|
||||
|
||||
const SPINNER_STRINGS: &[&str] = &[
|
||||
"[= ]",
|
||||
"[ = ]",
|
||||
"[ = ]",
|
||||
"[ = ]",
|
||||
"[ = ]",
|
||||
"[ = ]",
|
||||
"[ = ]",
|
||||
"[ = ]",
|
||||
"[ = ]",
|
||||
"[ = ]",
|
||||
"[ = ]",
|
||||
"[ = ]",
|
||||
"[ = ]",
|
||||
"[ = ]",
|
||||
"[ = ]",
|
||||
"[ = ]",
|
||||
"[ = ]",
|
||||
"[ = ]",
|
||||
"[ = ]",
|
||||
"[ =]",
|
||||
"[====================]",
|
||||
];
|
||||
|
||||
fn progress_bar_for_indication(indication: &ImageProgressIndication) -> Option<ProgressBar> {
|
||||
match indication.indication.as_ref() {
|
||||
Some(Indication::Hidden(_)) | None => None,
|
||||
Some(Indication::Bar(indic)) => {
|
||||
let bar = ProgressBar::new(indic.total);
|
||||
bar.enable_steady_tick(Duration::from_millis(100));
|
||||
Some(bar)
|
||||
}
|
||||
Some(Indication::Spinner(_)) => {
|
||||
let bar = ProgressBar::new_spinner();
|
||||
bar.enable_steady_tick(Duration::from_millis(100));
|
||||
Some(bar)
|
||||
}
|
||||
Some(Indication::Completed(indic)) => {
|
||||
let bar = ProgressBar::new_spinner();
|
||||
bar.enable_steady_tick(Duration::from_millis(100));
|
||||
if !indic.message.is_empty() {
|
||||
bar.finish_with_message(indic.message.clone());
|
||||
} else {
|
||||
bar.finish()
|
||||
}
|
||||
Some(bar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn configure_for_indication(
|
||||
bar: &mut ProgressBar,
|
||||
multi_progress: &mut MultiProgress,
|
||||
indication: &ImageProgressIndication,
|
||||
top_phase: Option<ImageProgressPhase>,
|
||||
layer_phase: Option<ImageProgressLayerPhase>,
|
||||
layer_id: Option<&str>,
|
||||
) {
|
||||
let prefix = if let Some(phase) = top_phase {
|
||||
match phase {
|
||||
ImageProgressPhase::Unknown => "unknown",
|
||||
ImageProgressPhase::Started => "started",
|
||||
ImageProgressPhase::Resolving => "resolving",
|
||||
ImageProgressPhase::Resolved => "resolved",
|
||||
ImageProgressPhase::ConfigDownload => "downloading",
|
||||
ImageProgressPhase::LayerDownload => "downloading",
|
||||
ImageProgressPhase::Assemble => "assembling",
|
||||
ImageProgressPhase::Pack => "packing",
|
||||
ImageProgressPhase::Complete => "complete",
|
||||
}
|
||||
} else if let Some(phase) = layer_phase {
|
||||
match phase {
|
||||
ImageProgressLayerPhase::Unknown => "unknown",
|
||||
ImageProgressLayerPhase::Waiting => "waiting",
|
||||
ImageProgressLayerPhase::Downloading => "downloading",
|
||||
ImageProgressLayerPhase::Downloaded => "downloaded",
|
||||
ImageProgressLayerPhase::Extracting => "extracting",
|
||||
ImageProgressLayerPhase::Extracted => "extracted",
|
||||
}
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let prefix = prefix.to_string();
|
||||
|
||||
let id = if let Some(layer_id) = layer_id {
|
||||
let hash = if let Some((_, hash)) = layer_id.split_once(':') {
|
||||
hash
|
||||
} else {
|
||||
"unknown"
|
||||
};
|
||||
let small_hash = if hash.len() > 10 { &hash[0..10] } else { hash };
|
||||
Some(format!("{:width$}", small_hash, width = 10))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let prefix = if let Some(id) = id {
|
||||
format!("{} {:width$}", id, prefix, width = 11)
|
||||
} else {
|
||||
format!(" {:width$}", prefix, width = 11)
|
||||
};
|
||||
|
||||
match indication.indication.as_ref() {
|
||||
Some(Indication::Hidden(_)) | None => {
|
||||
multi_progress.remove(bar);
|
||||
return;
|
||||
}
|
||||
Some(Indication::Bar(indic)) => {
|
||||
if indic.is_bytes {
|
||||
bar.set_style(ProgressStyle::with_template("{prefix} [{bar:20}] {msg} {binary_bytes}/{binary_total_bytes} ({binary_bytes_per_sec}) eta: {eta}").unwrap().progress_chars("=>-"));
|
||||
} else {
|
||||
bar.set_style(
|
||||
ProgressStyle::with_template(
|
||||
"{prefix} [{bar:20} {msg} {human_pos}/{human_len} ({per_sec}/sec)",
|
||||
)
|
||||
.unwrap()
|
||||
.progress_chars("=>-"),
|
||||
);
|
||||
}
|
||||
bar.set_message(indic.message.clone());
|
||||
bar.set_position(indic.current);
|
||||
bar.set_length(indic.total);
|
||||
}
|
||||
Some(Indication::Spinner(indic)) => {
|
||||
bar.set_style(
|
||||
ProgressStyle::with_template("{prefix} {spinner} {msg}")
|
||||
.unwrap()
|
||||
.tick_strings(SPINNER_STRINGS),
|
||||
);
|
||||
bar.set_message(indic.message.clone());
|
||||
}
|
||||
Some(Indication::Completed(indic)) => {
|
||||
if bar.is_finished() {
|
||||
return;
|
||||
}
|
||||
bar.disable_steady_tick();
|
||||
bar.set_message(indic.message.clone());
|
||||
if indic.total != 0 {
|
||||
bar.set_position(indic.total);
|
||||
bar.set_length(indic.total);
|
||||
}
|
||||
if bar.style().get_tick_str(0).contains('=') {
|
||||
bar.set_style(
|
||||
ProgressStyle::with_template("{prefix} {spinner} {msg}")
|
||||
.unwrap()
|
||||
.tick_strings(SPINNER_STRINGS),
|
||||
);
|
||||
bar.finish_with_message(indic.message.clone());
|
||||
} else if indic.is_bytes {
|
||||
bar.set_style(
|
||||
ProgressStyle::with_template("{prefix} [{bar:20}] {msg} {binary_total_bytes}")
|
||||
.unwrap()
|
||||
.progress_chars("=>-"),
|
||||
);
|
||||
} else {
|
||||
bar.set_style(
|
||||
ProgressStyle::with_template("{prefix} [{bar:20}] {msg}")
|
||||
.unwrap()
|
||||
.progress_chars("=>-"),
|
||||
);
|
||||
}
|
||||
bar.tick();
|
||||
bar.enable_steady_tick(Duration::from_millis(100));
|
||||
}
|
||||
};
|
||||
|
||||
bar.set_prefix(prefix);
|
||||
bar.tick();
|
||||
}
|
||||
|
||||
pub async fn pull_interactive_progress(
|
||||
mut stream: Streaming<PullImageReply>,
|
||||
) -> Result<PullImageReply> {
|
||||
let mut multi_progress = MultiProgress::new();
|
||||
multi_progress.set_move_cursor(false);
|
||||
let mut progresses = HashMap::new();
|
||||
|
||||
while let Some(reply) = stream.next().await {
|
||||
let reply = match reply {
|
||||
Ok(reply) => reply,
|
||||
Err(error) => {
|
||||
multi_progress.clear()?;
|
||||
return Err(error.into());
|
||||
}
|
||||
};
|
||||
|
||||
if reply.progress.is_none() && !reply.digest.is_empty() {
|
||||
multi_progress.clear()?;
|
||||
return Ok(reply);
|
||||
}
|
||||
|
||||
let Some(oci) = reply.progress else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for layer in &oci.layers {
|
||||
let Some(ref indication) = layer.indication else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let bar = match progresses.entry(layer.id.clone()) {
|
||||
Entry::Occupied(entry) => Some(entry.into_mut()),
|
||||
|
||||
Entry::Vacant(entry) => {
|
||||
if let Some(bar) = progress_bar_for_indication(indication) {
|
||||
multi_progress.add(bar.clone());
|
||||
Some(entry.insert(bar))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(bar) = bar {
|
||||
configure_for_indication(
|
||||
bar,
|
||||
&mut multi_progress,
|
||||
indication,
|
||||
None,
|
||||
Some(layer.phase()),
|
||||
Some(&layer.id),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref indication) = oci.indication {
|
||||
let bar = match progresses.entry("root".to_string()) {
|
||||
Entry::Occupied(entry) => Some(entry.into_mut()),
|
||||
|
||||
Entry::Vacant(entry) => {
|
||||
if let Some(bar) = progress_bar_for_indication(indication) {
|
||||
multi_progress.add(bar.clone());
|
||||
Some(entry.insert(bar))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(bar) = bar {
|
||||
configure_for_indication(
|
||||
bar,
|
||||
&mut multi_progress,
|
||||
indication,
|
||||
Some(oci.phase()),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
multi_progress.clear()?;
|
||||
Err(anyhow!("never received final reply for image pull"))
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
[package]
|
||||
name = "krata-daemon"
|
||||
description = "Daemon for the krata isolation engine"
|
||||
license.workspace = true
|
||||
version.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
[dependencies]
|
||||
krata-advmac = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
async-stream = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
circular-buffer = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
ipnetwork = { workspace = true }
|
||||
krata = { path = "../krata", version = "^0.0.21" }
|
||||
krata-oci = { path = "../oci", version = "^0.0.21" }
|
||||
krata-runtime = { path = "../runtime", version = "^0.0.21" }
|
||||
log = { workspace = true }
|
||||
prost = { workspace = true }
|
||||
redb = { workspace = true }
|
||||
scopeguard = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
signal-hook = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
krata-tokio-tar = { workspace = true }
|
||||
tonic = { workspace = true, features = ["tls"] }
|
||||
uuid = { workspace = true }
|
||||
|
||||
[lib]
|
||||
name = "kratad"
|
||||
|
||||
[[bin]]
|
||||
name = "kratad"
|
||||
path = "bin/daemon.rs"
|
@ -1,39 +0,0 @@
|
||||
use std::{
|
||||
net::{SocketAddr, TcpStream},
|
||||
str::FromStr,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use env_logger::fmt::Target;
|
||||
use log::LevelFilter;
|
||||
|
||||
use kratad::command::DaemonCommand;
|
||||
|
||||
#[tokio::main(flavor = "multi_thread", worker_threads = 10)]
|
||||
async fn main() -> Result<()> {
|
||||
let mut builder = env_logger::Builder::new();
|
||||
builder
|
||||
.filter_level(LevelFilter::Info)
|
||||
.parse_default_env()
|
||||
.filter(Some("backhand::filesystem::writer"), LevelFilter::Warn);
|
||||
|
||||
if let Ok(f_addr) = std::env::var("KRATA_FLUENT_ADDR") {
|
||||
let target = SocketAddr::from_str(f_addr.as_str())?;
|
||||
builder.target(Target::Pipe(Box::new(TcpStream::connect(target)?)));
|
||||
}
|
||||
|
||||
builder.init();
|
||||
|
||||
mask_sighup()?;
|
||||
|
||||
let command = DaemonCommand::parse();
|
||||
command.run().await
|
||||
}
|
||||
|
||||
fn mask_sighup() -> Result<()> {
|
||||
let flag = Arc::new(AtomicBool::new(false));
|
||||
signal_hook::flag::register(signal_hook::consts::SIGHUP, flag)?;
|
||||
Ok(())
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::{CommandFactory, Parser};
|
||||
use krata::dial::ControlDialAddress;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::Daemon;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(version, about = "krata isolation engine daemon")]
|
||||
pub struct DaemonCommand {
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
default_value = "unix:///var/lib/krata/daemon.socket",
|
||||
help = "Listen address"
|
||||
)]
|
||||
listen: String,
|
||||
#[arg(short, long, default_value = "/var/lib/krata", help = "Storage path")]
|
||||
store: String,
|
||||
}
|
||||
|
||||
impl DaemonCommand {
|
||||
pub async fn run(self) -> Result<()> {
|
||||
let addr = ControlDialAddress::from_str(&self.listen)?;
|
||||
let mut daemon = Daemon::new(self.store.clone()).await?;
|
||||
daemon.listen(addr).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn version() -> String {
|
||||
DaemonCommand::command()
|
||||
.get_version()
|
||||
.unwrap_or("unknown")
|
||||
.to_string()
|
||||
}
|
||||
}
|
@ -1,124 +0,0 @@
|
||||
use std::{collections::HashMap, path::Path};
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
pub struct DaemonConfig {
|
||||
#[serde(default)]
|
||||
pub oci: OciConfig,
|
||||
#[serde(default)]
|
||||
pub pci: DaemonPciConfig,
|
||||
#[serde(default = "default_network")]
|
||||
pub network: DaemonNetworkConfig,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
pub struct OciConfig {
|
||||
#[serde(default)]
|
||||
pub seed: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
pub struct DaemonPciConfig {
|
||||
#[serde(default)]
|
||||
pub devices: HashMap<String, DaemonPciDeviceConfig>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct DaemonPciDeviceConfig {
|
||||
pub locations: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub permissive: bool,
|
||||
#[serde(default)]
|
||||
#[serde(rename = "msi-translate")]
|
||||
pub msi_translate: bool,
|
||||
#[serde(default)]
|
||||
#[serde(rename = "power-management")]
|
||||
pub power_management: bool,
|
||||
#[serde(default)]
|
||||
#[serde(rename = "rdm-reserve-policy")]
|
||||
pub rdm_reserve_policy: DaemonPciDeviceRdmReservePolicy,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
pub enum DaemonPciDeviceRdmReservePolicy {
|
||||
#[default]
|
||||
#[serde(rename = "strict")]
|
||||
Strict,
|
||||
#[serde(rename = "relaxed")]
|
||||
Relaxed,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
pub struct DaemonNetworkConfig {
|
||||
#[serde(default = "default_network_nameservers")]
|
||||
pub nameservers: Vec<String>,
|
||||
#[serde(default = "default_network_ipv4")]
|
||||
pub ipv4: DaemonIpv4NetworkConfig,
|
||||
#[serde(default = "default_network_ipv6")]
|
||||
pub ipv6: DaemonIpv6NetworkConfig,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
pub struct DaemonIpv4NetworkConfig {
|
||||
#[serde(default = "default_network_ipv4_subnet")]
|
||||
pub subnet: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
pub struct DaemonIpv6NetworkConfig {
|
||||
#[serde(default = "default_network_ipv6_subnet")]
|
||||
pub subnet: String,
|
||||
}
|
||||
|
||||
fn default_network() -> DaemonNetworkConfig {
|
||||
DaemonNetworkConfig {
|
||||
nameservers: default_network_nameservers(),
|
||||
ipv4: default_network_ipv4(),
|
||||
ipv6: default_network_ipv6(),
|
||||
}
|
||||
}
|
||||
|
||||
fn default_network_nameservers() -> Vec<String> {
|
||||
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(),
|
||||
]
|
||||
}
|
||||
|
||||
fn default_network_ipv4() -> DaemonIpv4NetworkConfig {
|
||||
DaemonIpv4NetworkConfig {
|
||||
subnet: default_network_ipv4_subnet(),
|
||||
}
|
||||
}
|
||||
|
||||
fn default_network_ipv4_subnet() -> String {
|
||||
"10.75.0.0/16".to_string()
|
||||
}
|
||||
|
||||
fn default_network_ipv6() -> DaemonIpv6NetworkConfig {
|
||||
DaemonIpv6NetworkConfig {
|
||||
subnet: default_network_ipv6_subnet(),
|
||||
}
|
||||
}
|
||||
|
||||
fn default_network_ipv6_subnet() -> String {
|
||||
"fdd4:1476:6c7e::/48".to_string()
|
||||
}
|
||||
|
||||
impl DaemonConfig {
|
||||
pub async fn load(path: &Path) -> Result<DaemonConfig> {
|
||||
if !path.exists() {
|
||||
let config: DaemonConfig = toml::from_str("")?;
|
||||
let content = toml::to_string_pretty(&config)?;
|
||||
fs::write(&path, content).await?;
|
||||
}
|
||||
let content = fs::read_to_string(path).await?;
|
||||
let config: DaemonConfig = toml::from_str(&content)?;
|
||||
Ok(config)
|
||||
}
|
||||
}
|
@ -1,164 +0,0 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use circular_buffer::CircularBuffer;
|
||||
use kratart::channel::ChannelService;
|
||||
use log::error;
|
||||
use tokio::{
|
||||
sync::{
|
||||
mpsc::{error::TrySendError, Receiver, Sender},
|
||||
Mutex,
|
||||
},
|
||||
task::JoinHandle,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::zlt::ZoneLookupTable;
|
||||
|
||||
const CONSOLE_BUFFER_SIZE: usize = 1024 * 1024;
|
||||
type RawConsoleBuffer = CircularBuffer<CONSOLE_BUFFER_SIZE, u8>;
|
||||
type ConsoleBuffer = Box<RawConsoleBuffer>;
|
||||
|
||||
type ListenerMap = Arc<Mutex<HashMap<u32, Vec<Sender<Vec<u8>>>>>>;
|
||||
type BufferMap = Arc<Mutex<HashMap<u32, ConsoleBuffer>>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DaemonConsoleHandle {
|
||||
zlt: ZoneLookupTable,
|
||||
listeners: ListenerMap,
|
||||
buffers: BufferMap,
|
||||
sender: Sender<(u32, Vec<u8>)>,
|
||||
task: Arc<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DaemonConsoleAttachHandle {
|
||||
pub initial: Vec<u8>,
|
||||
listeners: ListenerMap,
|
||||
sender: Sender<(u32, Vec<u8>)>,
|
||||
domid: u32,
|
||||
}
|
||||
|
||||
impl DaemonConsoleAttachHandle {
|
||||
pub async fn unsubscribe(&self) -> Result<()> {
|
||||
let mut guard = self.listeners.lock().await;
|
||||
let _ = guard.remove(&self.domid);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send(&self, data: Vec<u8>) -> Result<()> {
|
||||
Ok(self.sender.send((self.domid, data)).await?)
|
||||
}
|
||||
}
|
||||
|
||||
impl DaemonConsoleHandle {
|
||||
pub async fn attach(
|
||||
&self,
|
||||
uuid: Uuid,
|
||||
sender: Sender<Vec<u8>>,
|
||||
) -> Result<DaemonConsoleAttachHandle> {
|
||||
let Some(domid) = self.zlt.lookup_domid_by_uuid(&uuid).await else {
|
||||
return Err(anyhow!("unable to find domain {}", uuid));
|
||||
};
|
||||
let buffers = self.buffers.lock().await;
|
||||
let buffer = buffers.get(&domid).map(|x| x.to_vec()).unwrap_or_default();
|
||||
drop(buffers);
|
||||
let mut listeners = self.listeners.lock().await;
|
||||
let senders = listeners.entry(domid).or_default();
|
||||
senders.push(sender);
|
||||
Ok(DaemonConsoleAttachHandle {
|
||||
initial: buffer,
|
||||
sender: self.sender.clone(),
|
||||
listeners: self.listeners.clone(),
|
||||
domid,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DaemonConsoleHandle {
|
||||
fn drop(&mut self) {
|
||||
if Arc::strong_count(&self.task) <= 1 {
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DaemonConsole {
|
||||
zlt: ZoneLookupTable,
|
||||
listeners: ListenerMap,
|
||||
buffers: BufferMap,
|
||||
receiver: Receiver<(u32, Option<Vec<u8>>)>,
|
||||
sender: Sender<(u32, Vec<u8>)>,
|
||||
task: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl DaemonConsole {
|
||||
pub async fn new(zlt: ZoneLookupTable) -> Result<DaemonConsole> {
|
||||
let (service, sender, receiver) =
|
||||
ChannelService::new("krata-console".to_string(), Some(0)).await?;
|
||||
let task = service.launch().await?;
|
||||
let listeners = Arc::new(Mutex::new(HashMap::new()));
|
||||
let buffers = Arc::new(Mutex::new(HashMap::new()));
|
||||
Ok(DaemonConsole {
|
||||
zlt,
|
||||
listeners,
|
||||
buffers,
|
||||
receiver,
|
||||
sender,
|
||||
task,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn launch(mut self) -> Result<DaemonConsoleHandle> {
|
||||
let zlt = self.zlt.clone();
|
||||
let listeners = self.listeners.clone();
|
||||
let buffers = self.buffers.clone();
|
||||
let sender = self.sender.clone();
|
||||
let task = tokio::task::spawn(async move {
|
||||
if let Err(error) = self.process().await {
|
||||
error!("failed to process console: {}", error);
|
||||
}
|
||||
});
|
||||
Ok(DaemonConsoleHandle {
|
||||
zlt,
|
||||
listeners,
|
||||
buffers,
|
||||
sender,
|
||||
task: Arc::new(task),
|
||||
})
|
||||
}
|
||||
|
||||
async fn process(&mut self) -> Result<()> {
|
||||
loop {
|
||||
let Some((domid, data)) = self.receiver.recv().await else {
|
||||
break;
|
||||
};
|
||||
|
||||
let mut buffers = self.buffers.lock().await;
|
||||
if let Some(data) = data {
|
||||
let buffer = buffers
|
||||
.entry(domid)
|
||||
.or_insert_with_key(|_| RawConsoleBuffer::boxed());
|
||||
buffer.extend_from_slice(&data);
|
||||
drop(buffers);
|
||||
let mut listeners = self.listeners.lock().await;
|
||||
if let Some(senders) = listeners.get_mut(&domid) {
|
||||
senders.retain(|sender| {
|
||||
!matches!(sender.try_send(data.to_vec()), Err(TrySendError::Closed(_)))
|
||||
});
|
||||
}
|
||||
} else {
|
||||
buffers.remove(&domid);
|
||||
let mut listeners = self.listeners.lock().await;
|
||||
listeners.remove(&domid);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DaemonConsole {
|
||||
fn drop(&mut self) {
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
use std::pin::Pin;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_stream::try_stream;
|
||||
use tokio::select;
|
||||
use tokio::sync::mpsc::channel;
|
||||
use tokio_stream::{Stream, StreamExt};
|
||||
use tonic::{Status, Streaming};
|
||||
use uuid::Uuid;
|
||||
|
||||
use krata::v1::control::{ZoneConsoleReply, ZoneConsoleRequest};
|
||||
|
||||
use crate::console::DaemonConsoleHandle;
|
||||
use crate::control::ApiError;
|
||||
|
||||
enum ConsoleDataSelect {
|
||||
Read(Option<Vec<u8>>),
|
||||
Write(Option<Result<ZoneConsoleRequest, Status>>),
|
||||
}
|
||||
|
||||
pub struct AttachZoneConsoleRpc {
|
||||
console: DaemonConsoleHandle,
|
||||
}
|
||||
|
||||
impl AttachZoneConsoleRpc {
|
||||
pub fn new(console: DaemonConsoleHandle) -> Self {
|
||||
Self { console }
|
||||
}
|
||||
|
||||
pub async fn process(
|
||||
self,
|
||||
mut input: Streaming<ZoneConsoleRequest>,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = Result<ZoneConsoleReply, Status>> + Send + 'static>>>
|
||||
{
|
||||
let Some(request) = input.next().await else {
|
||||
return Err(anyhow!("expected to have at least one request"));
|
||||
};
|
||||
let request = request?;
|
||||
let uuid = Uuid::from_str(&request.zone_id)?;
|
||||
let (sender, mut receiver) = channel(100);
|
||||
let console = self
|
||||
.console
|
||||
.attach(uuid, sender)
|
||||
.await
|
||||
.map_err(|error| anyhow!("failed to attach to console: {}", error))?;
|
||||
|
||||
let output = try_stream! {
|
||||
if request.replay_history {
|
||||
yield ZoneConsoleReply { data: console.initial.clone(), };
|
||||
}
|
||||
loop {
|
||||
let what = select! {
|
||||
x = receiver.recv() => ConsoleDataSelect::Read(x),
|
||||
x = input.next() => ConsoleDataSelect::Write(x),
|
||||
};
|
||||
|
||||
match what {
|
||||
ConsoleDataSelect::Read(Some(data)) => {
|
||||
yield ZoneConsoleReply { data, };
|
||||
},
|
||||
|
||||
ConsoleDataSelect::Read(None) => {
|
||||
break;
|
||||
}
|
||||
|
||||
ConsoleDataSelect::Write(Some(request)) => {
|
||||
let request = request?;
|
||||
if !request.data.is_empty() {
|
||||
console.send(request.data).await.map_err(|error| ApiError {
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
}
|
||||
},
|
||||
|
||||
ConsoleDataSelect::Write(None) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(Box::pin(output))
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
use crate::db::zone::ZoneStore;
|
||||
use crate::zlt::ZoneLookupTable;
|
||||
use anyhow::{anyhow, Result};
|
||||
use krata::v1::common::{Zone, ZoneState, ZoneStatus};
|
||||
use krata::v1::control::{CreateZoneReply, CreateZoneRequest};
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct CreateZoneRpc {
|
||||
zones: ZoneStore,
|
||||
zlt: ZoneLookupTable,
|
||||
zone_reconciler_notify: Sender<Uuid>,
|
||||
}
|
||||
|
||||
impl CreateZoneRpc {
|
||||
pub fn new(
|
||||
zones: ZoneStore,
|
||||
zlt: ZoneLookupTable,
|
||||
zone_reconciler_notify: Sender<Uuid>,
|
||||
) -> Self {
|
||||
Self {
|
||||
zones,
|
||||
zlt,
|
||||
zone_reconciler_notify,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn process(self, request: CreateZoneRequest) -> Result<CreateZoneReply> {
|
||||
let Some(spec) = request.spec else {
|
||||
return Err(anyhow!("zone spec not provided"));
|
||||
};
|
||||
let uuid = Uuid::new_v4();
|
||||
self.zones
|
||||
.update(
|
||||
uuid,
|
||||
Zone {
|
||||
id: uuid.to_string(),
|
||||
status: Some(ZoneStatus {
|
||||
state: ZoneState::Creating.into(),
|
||||
network_status: None,
|
||||
exit_status: None,
|
||||
error_status: None,
|
||||
resource_status: None,
|
||||
host: self.zlt.host_uuid().to_string(),
|
||||
domid: u32::MAX,
|
||||
}),
|
||||
spec: Some(spec),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
self.zone_reconciler_notify.send(uuid).await?;
|
||||
Ok(CreateZoneReply {
|
||||
zone_id: uuid.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use uuid::Uuid;
|
||||
|
||||
use krata::v1::common::ZoneState;
|
||||
use krata::v1::control::{DestroyZoneReply, DestroyZoneRequest};
|
||||
|
||||
use crate::db::zone::ZoneStore;
|
||||
|
||||
pub struct DestroyZoneRpc {
|
||||
zones: ZoneStore,
|
||||
zone_reconciler_notify: Sender<Uuid>,
|
||||
}
|
||||
|
||||
impl DestroyZoneRpc {
|
||||
pub fn new(zones: ZoneStore, zone_reconciler_notify: Sender<Uuid>) -> Self {
|
||||
Self {
|
||||
zones,
|
||||
zone_reconciler_notify,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn process(self, request: DestroyZoneRequest) -> Result<DestroyZoneReply> {
|
||||
let uuid = Uuid::from_str(&request.zone_id)?;
|
||||
let Some(mut zone) = self.zones.read(uuid).await? else {
|
||||
return Err(anyhow!("zone not found"));
|
||||
};
|
||||
|
||||
zone.status = Some(zone.status.as_mut().cloned().unwrap_or_default());
|
||||
|
||||
if zone.status.as_ref().unwrap().state() == ZoneState::Destroyed {
|
||||
return Err(anyhow!("zone already destroyed"));
|
||||
}
|
||||
|
||||
zone.status.as_mut().unwrap().state = ZoneState::Destroying.into();
|
||||
self.zones.update(uuid, zone).await?;
|
||||
self.zone_reconciler_notify.send(uuid).await?;
|
||||
Ok(DestroyZoneReply {})
|
||||
}
|
||||
}
|
@ -1,133 +0,0 @@
|
||||
use std::pin::Pin;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_stream::try_stream;
|
||||
use tokio::select;
|
||||
use tokio_stream::{Stream, StreamExt};
|
||||
use tonic::{Status, Streaming};
|
||||
use uuid::Uuid;
|
||||
|
||||
use krata::idm::internal::Request;
|
||||
use krata::{
|
||||
idm::internal::{
|
||||
exec_stream_request_update::Update, request::Request as IdmRequestType,
|
||||
response::Response as IdmResponseType, ExecEnvVar, ExecStreamRequestStart,
|
||||
ExecStreamRequestStdin, ExecStreamRequestTerminalSize, ExecStreamRequestUpdate,
|
||||
Request as IdmRequest,
|
||||
},
|
||||
v1::control::{ExecInsideZoneReply, ExecInsideZoneRequest},
|
||||
};
|
||||
|
||||
use crate::control::ApiError;
|
||||
use crate::idm::DaemonIdmHandle;
|
||||
|
||||
pub struct ExecInsideZoneRpc {
|
||||
idm: DaemonIdmHandle,
|
||||
}
|
||||
|
||||
impl ExecInsideZoneRpc {
|
||||
pub fn new(idm: DaemonIdmHandle) -> Self {
|
||||
Self { idm }
|
||||
}
|
||||
|
||||
pub async fn process(
|
||||
self,
|
||||
mut input: Streaming<ExecInsideZoneRequest>,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = Result<ExecInsideZoneReply, Status>> + Send + 'static>>>
|
||||
{
|
||||
let Some(request) = input.next().await else {
|
||||
return Err(anyhow!("expected to have at least one request"));
|
||||
};
|
||||
let request = request?;
|
||||
|
||||
let Some(task) = request.task else {
|
||||
return Err(anyhow!("task is missing"));
|
||||
};
|
||||
|
||||
let uuid = Uuid::from_str(&request.zone_id)?;
|
||||
let idm = self.idm.client(uuid).await?;
|
||||
|
||||
let idm_request = Request {
|
||||
request: Some(IdmRequestType::ExecStream(ExecStreamRequestUpdate {
|
||||
update: Some(Update::Start(ExecStreamRequestStart {
|
||||
environment: task
|
||||
.environment
|
||||
.into_iter()
|
||||
.map(|x| ExecEnvVar {
|
||||
key: x.key,
|
||||
value: x.value,
|
||||
})
|
||||
.collect(),
|
||||
command: task.command,
|
||||
working_directory: task.working_directory,
|
||||
tty: task.tty,
|
||||
terminal_size: request.terminal_size.map(|size| {
|
||||
ExecStreamRequestTerminalSize {
|
||||
rows: size.rows,
|
||||
columns: size.columns,
|
||||
}
|
||||
}),
|
||||
})),
|
||||
})),
|
||||
};
|
||||
|
||||
let output = try_stream! {
|
||||
let mut handle = idm.send_stream(idm_request).await.map_err(|x| ApiError {
|
||||
message: x.to_string(),
|
||||
})?;
|
||||
|
||||
loop {
|
||||
select! {
|
||||
x = input.next() => if let Some(update) = x {
|
||||
let update: Result<ExecInsideZoneRequest, Status> = update.map_err(|error| ApiError {
|
||||
message: error.to_string()
|
||||
}.into());
|
||||
|
||||
if let Ok(update) = update {
|
||||
if !update.stdin.is_empty() {
|
||||
let _ = handle.update(IdmRequest {
|
||||
request: Some(IdmRequestType::ExecStream(ExecStreamRequestUpdate {
|
||||
update: Some(Update::Stdin(ExecStreamRequestStdin {
|
||||
data: update.stdin,
|
||||
closed: update.stdin_closed,
|
||||
})),
|
||||
}))}).await;
|
||||
}
|
||||
|
||||
if let Some(ref terminal_size) = update.terminal_size {
|
||||
let _ = handle.update(IdmRequest {
|
||||
request: Some(IdmRequestType::ExecStream(ExecStreamRequestUpdate {
|
||||
update: Some(Update::TerminalResize(ExecStreamRequestTerminalSize {
|
||||
rows: terminal_size.rows,
|
||||
columns: terminal_size.columns,
|
||||
})),
|
||||
}))}).await;
|
||||
}
|
||||
}
|
||||
},
|
||||
x = handle.receiver.recv() => match x {
|
||||
Some(response) => {
|
||||
let Some(IdmResponseType::ExecStream(update)) = response.response else {
|
||||
break;
|
||||
};
|
||||
let reply = ExecInsideZoneReply {
|
||||
exited: update.exited,
|
||||
error: update.error,
|
||||
exit_code: update.exit_code,
|
||||
stdout: update.stdout,
|
||||
stderr: update.stderr,
|
||||
};
|
||||
yield reply;
|
||||
},
|
||||
None => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Box::pin(output))
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use krata::v1::control::{GetHostCpuTopologyReply, GetHostCpuTopologyRequest, HostCpuTopologyInfo};
|
||||
use kratart::Runtime;
|
||||
|
||||
pub struct GetHostCpuTopologyRpc {
|
||||
runtime: Runtime,
|
||||
}
|
||||
|
||||
impl GetHostCpuTopologyRpc {
|
||||
pub fn new(runtime: Runtime) -> Self {
|
||||
Self { runtime }
|
||||
}
|
||||
|
||||
pub async fn process(
|
||||
self,
|
||||
_request: GetHostCpuTopologyRequest,
|
||||
) -> Result<GetHostCpuTopologyReply> {
|
||||
let power = self.runtime.power_management_context().await?;
|
||||
let cpu_topology = power.cpu_topology().await?;
|
||||
let mut cpus = vec![];
|
||||
|
||||
for cpu in cpu_topology {
|
||||
cpus.push(HostCpuTopologyInfo {
|
||||
core: cpu.core,
|
||||
socket: cpu.socket,
|
||||
node: cpu.node,
|
||||
thread: cpu.thread,
|
||||
class: cpu.class as i32,
|
||||
})
|
||||
}
|
||||
Ok(GetHostCpuTopologyReply { cpus })
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
use crate::command::DaemonCommand;
|
||||
use crate::network::assignment::NetworkAssignment;
|
||||
use crate::zlt::ZoneLookupTable;
|
||||
use anyhow::Result;
|
||||
use krata::v1::control::{GetHostStatusReply, GetHostStatusRequest};
|
||||
|
||||
pub struct GetHostStatusRpc {
|
||||
network: NetworkAssignment,
|
||||
zlt: ZoneLookupTable,
|
||||
}
|
||||
|
||||
impl GetHostStatusRpc {
|
||||
pub fn new(ip: NetworkAssignment, zlt: ZoneLookupTable) -> Self {
|
||||
Self { network: ip, zlt }
|
||||
}
|
||||
|
||||
pub async fn process(self, _request: GetHostStatusRequest) -> Result<GetHostStatusReply> {
|
||||
let host_reservation = self.network.retrieve(self.zlt.host_uuid()).await?;
|
||||
Ok(GetHostStatusReply {
|
||||
host_domid: self.zlt.host_domid(),
|
||||
host_uuid: self.zlt.host_uuid().to_string(),
|
||||
krata_version: DaemonCommand::version(),
|
||||
host_ipv4: host_reservation
|
||||
.as_ref()
|
||||
.map(|x| format!("{}/{}", x.ipv4, x.ipv4_prefix))
|
||||
.unwrap_or_default(),
|
||||
host_ipv6: host_reservation
|
||||
.as_ref()
|
||||
.map(|x| format!("{}/{}", x.ipv6, x.ipv6_prefix))
|
||||
.unwrap_or_default(),
|
||||
host_mac: host_reservation
|
||||
.as_ref()
|
||||
.map(|x| x.mac.to_string().to_lowercase().replace('-', ":"))
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Result;
|
||||
use uuid::Uuid;
|
||||
|
||||
use krata::v1::control::{GetZoneReply, GetZoneRequest};
|
||||
|
||||
use crate::db::zone::ZoneStore;
|
||||
|
||||
pub struct GetZoneRpc {
|
||||
zones: ZoneStore,
|
||||
}
|
||||
|
||||
impl GetZoneRpc {
|
||||
pub fn new(zones: ZoneStore) -> Self {
|
||||
Self { zones }
|
||||
}
|
||||
|
||||
pub async fn process(self, request: GetZoneRequest) -> Result<GetZoneReply> {
|
||||
let mut zones = self.zones.list().await?;
|
||||
let zone = zones.remove(&Uuid::from_str(&request.zone_id)?);
|
||||
Ok(GetZoneReply { zone })
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use krata::v1::control::{DeviceInfo, ListDevicesReply, ListDevicesRequest};
|
||||
|
||||
use crate::devices::DaemonDeviceManager;
|
||||
|
||||
pub struct ListDevicesRpc {
|
||||
devices: DaemonDeviceManager,
|
||||
}
|
||||
|
||||
impl ListDevicesRpc {
|
||||
pub fn new(devices: DaemonDeviceManager) -> Self {
|
||||
Self { devices }
|
||||
}
|
||||
|
||||
pub async fn process(self, _request: ListDevicesRequest) -> Result<ListDevicesReply> {
|
||||
let mut devices = Vec::new();
|
||||
let state = self.devices.copy().await?;
|
||||
for (name, state) in state {
|
||||
devices.push(DeviceInfo {
|
||||
name,
|
||||
claimed: state.owner.is_some(),
|
||||
owner: state.owner.map(|x| x.to_string()).unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
Ok(ListDevicesReply { devices })
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use krata::v1::{
|
||||
common::NetworkReservation,
|
||||
control::{ListNetworkReservationsReply, ListNetworkReservationsRequest},
|
||||
};
|
||||
|
||||
use crate::network::assignment::NetworkAssignment;
|
||||
|
||||
pub struct ListNetworkReservationsRpc {
|
||||
network: NetworkAssignment,
|
||||
}
|
||||
|
||||
impl ListNetworkReservationsRpc {
|
||||
pub fn new(network: NetworkAssignment) -> Self {
|
||||
Self { network }
|
||||
}
|
||||
|
||||
pub async fn process(
|
||||
self,
|
||||
_request: ListNetworkReservationsRequest,
|
||||
) -> Result<ListNetworkReservationsReply> {
|
||||
let state = self.network.read_reservations().await?;
|
||||
let reservations: Vec<NetworkReservation> =
|
||||
state.into_values().map(|x| x.into()).collect::<Vec<_>>();
|
||||
Ok(ListNetworkReservationsReply { reservations })
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use krata::v1::common::Zone;
|
||||
use krata::v1::control::{ListZonesReply, ListZonesRequest};
|
||||
|
||||
use crate::db::zone::ZoneStore;
|
||||
|
||||
pub struct ListZonesRpc {
|
||||
zones: ZoneStore,
|
||||
}
|
||||
|
||||
impl ListZonesRpc {
|
||||
pub fn new(zones: ZoneStore) -> Self {
|
||||
Self { zones }
|
||||
}
|
||||
|
||||
pub async fn process(self, _request: ListZonesRequest) -> Result<ListZonesReply> {
|
||||
let zones = self.zones.list().await?;
|
||||
let zones = zones.into_values().collect::<Vec<Zone>>();
|
||||
Ok(ListZonesReply { zones })
|
||||
}
|
||||
}
|
@ -1,365 +0,0 @@
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::Error;
|
||||
use futures::Stream;
|
||||
use list_network_reservations::ListNetworkReservationsRpc;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tonic::{Request, Response, Status, Streaming};
|
||||
use uuid::Uuid;
|
||||
|
||||
use krata::v1::control::{
|
||||
control_service_server::ControlService, CreateZoneReply, CreateZoneRequest, DestroyZoneReply,
|
||||
DestroyZoneRequest, ExecInsideZoneReply, ExecInsideZoneRequest, GetHostCpuTopologyReply,
|
||||
GetHostCpuTopologyRequest, GetHostStatusReply, GetHostStatusRequest, ListDevicesReply,
|
||||
ListDevicesRequest, ListZonesReply, ListZonesRequest, PullImageReply, PullImageRequest,
|
||||
ReadHypervisorConsoleReply, ReadHypervisorConsoleRequest, ReadZoneMetricsReply,
|
||||
ReadZoneMetricsRequest, ResolveZoneIdReply, ResolveZoneIdRequest, SnoopIdmReply,
|
||||
SnoopIdmRequest, UpdateZoneResourcesReply, UpdateZoneResourcesRequest, WatchEventsReply,
|
||||
WatchEventsRequest, ZoneConsoleReply, ZoneConsoleRequest,
|
||||
};
|
||||
use krata::v1::control::{
|
||||
GetZoneReply, GetZoneRequest, ListNetworkReservationsReply, ListNetworkReservationsRequest,
|
||||
SetHostPowerManagementPolicyReply, SetHostPowerManagementPolicyRequest,
|
||||
};
|
||||
use krataoci::packer::service::OciPackerService;
|
||||
use kratart::Runtime;
|
||||
|
||||
use crate::control::attach_zone_console::AttachZoneConsoleRpc;
|
||||
use crate::control::create_zone::CreateZoneRpc;
|
||||
use crate::control::destroy_zone::DestroyZoneRpc;
|
||||
use crate::control::exec_inside_zone::ExecInsideZoneRpc;
|
||||
use crate::control::get_host_cpu_topology::GetHostCpuTopologyRpc;
|
||||
use crate::control::get_host_status::GetHostStatusRpc;
|
||||
use crate::control::get_zone::GetZoneRpc;
|
||||
use crate::control::list_devices::ListDevicesRpc;
|
||||
use crate::control::list_zones::ListZonesRpc;
|
||||
use crate::control::pull_image::PullImageRpc;
|
||||
use crate::control::read_hypervisor_console::ReadHypervisorConsoleRpc;
|
||||
use crate::control::read_zone_metrics::ReadZoneMetricsRpc;
|
||||
use crate::control::resolve_zone_id::ResolveZoneIdRpc;
|
||||
use crate::control::set_host_power_management_policy::SetHostPowerManagementPolicyRpc;
|
||||
use crate::control::snoop_idm::SnoopIdmRpc;
|
||||
use crate::control::update_zone_resources::UpdateZoneResourcesRpc;
|
||||
use crate::control::watch_events::WatchEventsRpc;
|
||||
use crate::db::zone::ZoneStore;
|
||||
use crate::network::assignment::NetworkAssignment;
|
||||
use crate::{
|
||||
console::DaemonConsoleHandle, devices::DaemonDeviceManager, event::DaemonEventContext,
|
||||
idm::DaemonIdmHandle, zlt::ZoneLookupTable,
|
||||
};
|
||||
|
||||
pub mod attach_zone_console;
|
||||
pub mod create_zone;
|
||||
pub mod destroy_zone;
|
||||
pub mod exec_inside_zone;
|
||||
pub mod get_host_cpu_topology;
|
||||
pub mod get_host_status;
|
||||
pub mod get_zone;
|
||||
pub mod list_devices;
|
||||
pub mod list_network_reservations;
|
||||
pub mod list_zones;
|
||||
pub mod pull_image;
|
||||
pub mod read_hypervisor_console;
|
||||
pub mod read_zone_metrics;
|
||||
pub mod resolve_zone_id;
|
||||
pub mod set_host_power_management_policy;
|
||||
pub mod snoop_idm;
|
||||
pub mod update_zone_resources;
|
||||
pub mod watch_events;
|
||||
|
||||
pub struct ApiError {
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl From<Error> for ApiError {
|
||||
fn from(value: Error) -> Self {
|
||||
ApiError {
|
||||
message: value.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApiError> for Status {
|
||||
fn from(value: ApiError) -> Self {
|
||||
Status::unknown(value.message)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DaemonControlService {
|
||||
zlt: ZoneLookupTable,
|
||||
devices: DaemonDeviceManager,
|
||||
events: DaemonEventContext,
|
||||
console: DaemonConsoleHandle,
|
||||
idm: DaemonIdmHandle,
|
||||
zones: ZoneStore,
|
||||
network: NetworkAssignment,
|
||||
zone_reconciler_notify: Sender<Uuid>,
|
||||
packer: OciPackerService,
|
||||
runtime: Runtime,
|
||||
}
|
||||
|
||||
impl DaemonControlService {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
zlt: ZoneLookupTable,
|
||||
devices: DaemonDeviceManager,
|
||||
events: DaemonEventContext,
|
||||
console: DaemonConsoleHandle,
|
||||
idm: DaemonIdmHandle,
|
||||
zones: ZoneStore,
|
||||
network: NetworkAssignment,
|
||||
zone_reconciler_notify: Sender<Uuid>,
|
||||
packer: OciPackerService,
|
||||
runtime: Runtime,
|
||||
) -> Self {
|
||||
Self {
|
||||
zlt,
|
||||
devices,
|
||||
events,
|
||||
console,
|
||||
idm,
|
||||
zones,
|
||||
network,
|
||||
zone_reconciler_notify,
|
||||
packer,
|
||||
runtime,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl ControlService for DaemonControlService {
|
||||
async fn get_host_status(
|
||||
&self,
|
||||
request: Request<GetHostStatusRequest>,
|
||||
) -> Result<Response<GetHostStatusReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(
|
||||
GetHostStatusRpc::new(self.network.clone(), self.zlt.clone())
|
||||
.process(request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
type SnoopIdmStream =
|
||||
Pin<Box<dyn Stream<Item = Result<SnoopIdmReply, Status>> + Send + 'static>>;
|
||||
|
||||
async fn snoop_idm(
|
||||
&self,
|
||||
request: Request<SnoopIdmRequest>,
|
||||
) -> Result<Response<Self::SnoopIdmStream>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(
|
||||
SnoopIdmRpc::new(self.idm.clone(), self.zlt.clone())
|
||||
.process(request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_host_cpu_topology(
|
||||
&self,
|
||||
request: Request<GetHostCpuTopologyRequest>,
|
||||
) -> Result<Response<GetHostCpuTopologyReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(
|
||||
GetHostCpuTopologyRpc::new(self.runtime.clone())
|
||||
.process(request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn set_host_power_management_policy(
|
||||
&self,
|
||||
request: Request<SetHostPowerManagementPolicyRequest>,
|
||||
) -> Result<Response<SetHostPowerManagementPolicyReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(
|
||||
SetHostPowerManagementPolicyRpc::new(self.runtime.clone())
|
||||
.process(request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn list_devices(
|
||||
&self,
|
||||
request: Request<ListDevicesRequest>,
|
||||
) -> Result<Response<ListDevicesReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(
|
||||
ListDevicesRpc::new(self.devices.clone())
|
||||
.process(request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn list_network_reservations(
|
||||
&self,
|
||||
request: Request<ListNetworkReservationsRequest>,
|
||||
) -> Result<Response<ListNetworkReservationsReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(
|
||||
ListNetworkReservationsRpc::new(self.network.clone())
|
||||
.process(request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
type PullImageStream =
|
||||
Pin<Box<dyn Stream<Item = Result<PullImageReply, Status>> + Send + 'static>>;
|
||||
|
||||
async fn pull_image(
|
||||
&self,
|
||||
request: Request<PullImageRequest>,
|
||||
) -> Result<Response<Self::PullImageStream>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(
|
||||
PullImageRpc::new(self.packer.clone())
|
||||
.process(request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn create_zone(
|
||||
&self,
|
||||
request: Request<CreateZoneRequest>,
|
||||
) -> Result<Response<CreateZoneReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(
|
||||
CreateZoneRpc::new(
|
||||
self.zones.clone(),
|
||||
self.zlt.clone(),
|
||||
self.zone_reconciler_notify.clone(),
|
||||
)
|
||||
.process(request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn destroy_zone(
|
||||
&self,
|
||||
request: Request<DestroyZoneRequest>,
|
||||
) -> Result<Response<DestroyZoneReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(
|
||||
DestroyZoneRpc::new(self.zones.clone(), self.zone_reconciler_notify.clone())
|
||||
.process(request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn resolve_zone_id(
|
||||
&self,
|
||||
request: Request<ResolveZoneIdRequest>,
|
||||
) -> Result<Response<ResolveZoneIdReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(
|
||||
ResolveZoneIdRpc::new(self.zones.clone())
|
||||
.process(request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_zone(
|
||||
&self,
|
||||
request: Request<GetZoneRequest>,
|
||||
) -> Result<Response<GetZoneReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(GetZoneRpc::new(self.zones.clone()).process(request).await)
|
||||
}
|
||||
|
||||
async fn update_zone_resources(
|
||||
&self,
|
||||
request: Request<UpdateZoneResourcesRequest>,
|
||||
) -> Result<Response<UpdateZoneResourcesReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(
|
||||
UpdateZoneResourcesRpc::new(self.runtime.clone(), self.zones.clone())
|
||||
.process(request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn list_zones(
|
||||
&self,
|
||||
request: Request<ListZonesRequest>,
|
||||
) -> Result<Response<ListZonesReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(ListZonesRpc::new(self.zones.clone()).process(request).await)
|
||||
}
|
||||
|
||||
type AttachZoneConsoleStream =
|
||||
Pin<Box<dyn Stream<Item = Result<ZoneConsoleReply, Status>> + Send + 'static>>;
|
||||
|
||||
async fn attach_zone_console(
|
||||
&self,
|
||||
request: Request<Streaming<ZoneConsoleRequest>>,
|
||||
) -> Result<Response<Self::AttachZoneConsoleStream>, Status> {
|
||||
let input = request.into_inner();
|
||||
adapt(
|
||||
AttachZoneConsoleRpc::new(self.console.clone())
|
||||
.process(input)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
type ExecInsideZoneStream =
|
||||
Pin<Box<dyn Stream<Item = Result<ExecInsideZoneReply, Status>> + Send + 'static>>;
|
||||
|
||||
async fn exec_inside_zone(
|
||||
&self,
|
||||
request: Request<Streaming<ExecInsideZoneRequest>>,
|
||||
) -> Result<Response<Self::ExecInsideZoneStream>, Status> {
|
||||
let input = request.into_inner();
|
||||
adapt(
|
||||
ExecInsideZoneRpc::new(self.idm.clone())
|
||||
.process(input)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn read_zone_metrics(
|
||||
&self,
|
||||
request: Request<ReadZoneMetricsRequest>,
|
||||
) -> Result<Response<ReadZoneMetricsReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(
|
||||
ReadZoneMetricsRpc::new(self.idm.clone())
|
||||
.process(request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
type WatchEventsStream =
|
||||
Pin<Box<dyn Stream<Item = Result<WatchEventsReply, Status>> + Send + 'static>>;
|
||||
|
||||
async fn watch_events(
|
||||
&self,
|
||||
request: Request<WatchEventsRequest>,
|
||||
) -> Result<Response<Self::WatchEventsStream>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(
|
||||
WatchEventsRpc::new(self.events.clone())
|
||||
.process(request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn read_hypervisor_console(
|
||||
&self,
|
||||
request: Request<ReadHypervisorConsoleRequest>,
|
||||
) -> Result<Response<ReadHypervisorConsoleReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(
|
||||
ReadHypervisorConsoleRpc::new(self.runtime.clone())
|
||||
.process(request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn adapt<T>(result: anyhow::Result<T>) -> Result<Response<T>, Status> {
|
||||
result
|
||||
.map(Response::new)
|
||||
.map_err(|error| Status::unknown(error.to_string()))
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
use crate::control::ApiError;
|
||||
use crate::oci::convert_oci_progress;
|
||||
use anyhow::Result;
|
||||
use async_stream::try_stream;
|
||||
use krata::v1::common::OciImageFormat;
|
||||
use krata::v1::control::{PullImageReply, PullImageRequest};
|
||||
use krataoci::name::ImageName;
|
||||
use krataoci::packer::service::OciPackerService;
|
||||
use krataoci::packer::{OciPackedFormat, OciPackedImage};
|
||||
use krataoci::progress::{OciProgress, OciProgressContext};
|
||||
use std::pin::Pin;
|
||||
use tokio::select;
|
||||
use tokio::task::JoinError;
|
||||
use tokio_stream::Stream;
|
||||
use tonic::Status;
|
||||
|
||||
enum PullImageSelect {
|
||||
Progress(Option<OciProgress>),
|
||||
Completed(Result<Result<OciPackedImage, anyhow::Error>, JoinError>),
|
||||
}
|
||||
|
||||
pub struct PullImageRpc {
|
||||
packer: OciPackerService,
|
||||
}
|
||||
|
||||
impl PullImageRpc {
|
||||
pub fn new(packer: OciPackerService) -> Self {
|
||||
Self { packer }
|
||||
}
|
||||
|
||||
pub async fn process(
|
||||
self,
|
||||
request: PullImageRequest,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = Result<PullImageReply, Status>> + Send + 'static>>> {
|
||||
let name = ImageName::parse(&request.image)?;
|
||||
let format = match request.format() {
|
||||
OciImageFormat::Unknown => OciPackedFormat::Squashfs,
|
||||
OciImageFormat::Squashfs => OciPackedFormat::Squashfs,
|
||||
OciImageFormat::Erofs => OciPackedFormat::Erofs,
|
||||
OciImageFormat::Tar => OciPackedFormat::Tar,
|
||||
};
|
||||
let (context, mut receiver) = OciProgressContext::create();
|
||||
let our_packer = self.packer;
|
||||
|
||||
let output = try_stream! {
|
||||
let mut task = tokio::task::spawn(async move {
|
||||
our_packer.request(name, format, request.overwrite_cache, request.update, context).await
|
||||
});
|
||||
let abort_handle = task.abort_handle();
|
||||
let _task_cancel_guard = scopeguard::guard(abort_handle, |handle| {
|
||||
handle.abort();
|
||||
});
|
||||
|
||||
loop {
|
||||
let what = select! {
|
||||
x = receiver.changed() => match x {
|
||||
Ok(_) => PullImageSelect::Progress(Some(receiver.borrow_and_update().clone())),
|
||||
Err(_) => PullImageSelect::Progress(None),
|
||||
},
|
||||
x = &mut task => PullImageSelect::Completed(x),
|
||||
};
|
||||
match what {
|
||||
PullImageSelect::Progress(Some(progress)) => {
|
||||
let reply = PullImageReply {
|
||||
progress: Some(convert_oci_progress(progress)),
|
||||
digest: String::new(),
|
||||
format: OciImageFormat::Unknown.into(),
|
||||
};
|
||||
yield reply;
|
||||
},
|
||||
|
||||
PullImageSelect::Completed(result) => {
|
||||
let result = result.map_err(|err| ApiError {
|
||||
message: err.to_string(),
|
||||
})?;
|
||||
let packed = result.map_err(|err| ApiError {
|
||||
message: err.to_string(),
|
||||
})?;
|
||||
let reply = PullImageReply {
|
||||
progress: None,
|
||||
digest: packed.digest,
|
||||
format: match packed.format {
|
||||
OciPackedFormat::Squashfs => OciImageFormat::Squashfs.into(),
|
||||
OciPackedFormat::Erofs => OciImageFormat::Erofs.into(),
|
||||
OciPackedFormat::Tar => OciImageFormat::Tar.into(),
|
||||
},
|
||||
};
|
||||
yield reply;
|
||||
break;
|
||||
},
|
||||
|
||||
_ => {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(Box::pin(output))
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use krata::v1::control::{ReadHypervisorConsoleReply, ReadHypervisorConsoleRequest};
|
||||
use kratart::Runtime;
|
||||
|
||||
pub struct ReadHypervisorConsoleRpc {
|
||||
runtime: Runtime,
|
||||
}
|
||||
|
||||
impl ReadHypervisorConsoleRpc {
|
||||
pub fn new(runtime: Runtime) -> Self {
|
||||
Self { runtime }
|
||||
}
|
||||
|
||||
pub async fn process(
|
||||
self,
|
||||
_: ReadHypervisorConsoleRequest,
|
||||
) -> Result<ReadHypervisorConsoleReply> {
|
||||
let data = self.runtime.read_hypervisor_console(false).await?;
|
||||
Ok(ReadHypervisorConsoleReply {
|
||||
data: data.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Result;
|
||||
use uuid::Uuid;
|
||||
|
||||
use krata::idm::internal::MetricsRequest;
|
||||
use krata::idm::internal::{
|
||||
request::Request as IdmRequestType, response::Response as IdmResponseType,
|
||||
Request as IdmRequest,
|
||||
};
|
||||
use krata::v1::control::{ReadZoneMetricsReply, ReadZoneMetricsRequest};
|
||||
|
||||
use crate::idm::DaemonIdmHandle;
|
||||
use crate::metrics::idm_metric_to_api;
|
||||
|
||||
pub struct ReadZoneMetricsRpc {
|
||||
idm: DaemonIdmHandle,
|
||||
}
|
||||
|
||||
impl ReadZoneMetricsRpc {
|
||||
pub fn new(idm: DaemonIdmHandle) -> Self {
|
||||
Self { idm }
|
||||
}
|
||||
|
||||
pub async fn process(self, request: ReadZoneMetricsRequest) -> Result<ReadZoneMetricsReply> {
|
||||
let uuid = Uuid::from_str(&request.zone_id)?;
|
||||
let client = self.idm.client(uuid).await?;
|
||||
let response = client
|
||||
.send(IdmRequest {
|
||||
request: Some(IdmRequestType::Metrics(MetricsRequest {})),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let mut reply = ReadZoneMetricsReply::default();
|
||||
if let Some(IdmResponseType::Metrics(metrics)) = response.response {
|
||||
reply.root = metrics.root.map(idm_metric_to_api);
|
||||
}
|
||||
Ok(reply)
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use krata::v1::common::Zone;
|
||||
use krata::v1::control::{ResolveZoneIdReply, ResolveZoneIdRequest};
|
||||
|
||||
use crate::db::zone::ZoneStore;
|
||||
|
||||
pub struct ResolveZoneIdRpc {
|
||||
zones: ZoneStore,
|
||||
}
|
||||
|
||||
impl ResolveZoneIdRpc {
|
||||
pub fn new(zones: ZoneStore) -> Self {
|
||||
Self { zones }
|
||||
}
|
||||
|
||||
pub async fn process(self, request: ResolveZoneIdRequest) -> Result<ResolveZoneIdReply> {
|
||||
let zones = self.zones.list().await?;
|
||||
let zones = zones
|
||||
.into_values()
|
||||
.filter(|x| {
|
||||
let comparison_spec = x.spec.as_ref().cloned().unwrap_or_default();
|
||||
(!request.name.is_empty() && comparison_spec.name == request.name)
|
||||
|| x.id == request.name
|
||||
})
|
||||
.collect::<Vec<Zone>>();
|
||||
Ok(ResolveZoneIdReply {
|
||||
zone_id: zones.first().cloned().map(|x| x.id).unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use krata::v1::control::{SetHostPowerManagementPolicyReply, SetHostPowerManagementPolicyRequest};
|
||||
use kratart::Runtime;
|
||||
|
||||
pub struct SetHostPowerManagementPolicyRpc {
|
||||
runtime: Runtime,
|
||||
}
|
||||
|
||||
impl SetHostPowerManagementPolicyRpc {
|
||||
pub fn new(runtime: Runtime) -> Self {
|
||||
Self { runtime }
|
||||
}
|
||||
|
||||
pub async fn process(
|
||||
self,
|
||||
request: SetHostPowerManagementPolicyRequest,
|
||||
) -> Result<SetHostPowerManagementPolicyReply> {
|
||||
let power = self.runtime.power_management_context().await?;
|
||||
let scheduler = &request.scheduler;
|
||||
|
||||
power.set_smt_policy(request.smt_awareness).await?;
|
||||
power.set_scheduler_policy(scheduler).await?;
|
||||
Ok(SetHostPowerManagementPolicyReply {})
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
use crate::idm::DaemonIdmHandle;
|
||||
use crate::zlt::ZoneLookupTable;
|
||||
use anyhow::Result;
|
||||
use async_stream::try_stream;
|
||||
use krata::v1::control::{SnoopIdmReply, SnoopIdmRequest};
|
||||
use std::pin::Pin;
|
||||
use tokio_stream::Stream;
|
||||
use tonic::Status;
|
||||
|
||||
pub struct SnoopIdmRpc {
|
||||
idm: DaemonIdmHandle,
|
||||
zlt: ZoneLookupTable,
|
||||
}
|
||||
|
||||
impl SnoopIdmRpc {
|
||||
pub fn new(idm: DaemonIdmHandle, zlt: ZoneLookupTable) -> Self {
|
||||
Self { idm, zlt }
|
||||
}
|
||||
|
||||
pub async fn process(
|
||||
self,
|
||||
_request: SnoopIdmRequest,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = Result<SnoopIdmReply, Status>> + Send + 'static>>> {
|
||||
let mut messages = self.idm.snoop();
|
||||
let zlt = self.zlt.clone();
|
||||
let output = try_stream! {
|
||||
while let Ok(event) = messages.recv().await {
|
||||
let Some(from_uuid) = zlt.lookup_uuid_by_domid(event.from).await else {
|
||||
continue;
|
||||
};
|
||||
let Some(to_uuid) = zlt.lookup_uuid_by_domid(event.to).await else {
|
||||
continue;
|
||||
};
|
||||
yield SnoopIdmReply { from: from_uuid.to_string(), to: to_uuid.to_string(), packet: Some(event.packet) };
|
||||
}
|
||||
};
|
||||
Ok(Box::pin(output))
|
||||
}
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use uuid::Uuid;
|
||||
|
||||
use krata::v1::common::{ZoneResourceStatus, ZoneState};
|
||||
use krata::v1::control::{UpdateZoneResourcesReply, UpdateZoneResourcesRequest};
|
||||
use kratart::Runtime;
|
||||
|
||||
use crate::db::zone::ZoneStore;
|
||||
|
||||
pub struct UpdateZoneResourcesRpc {
|
||||
runtime: Runtime,
|
||||
zones: ZoneStore,
|
||||
}
|
||||
|
||||
impl UpdateZoneResourcesRpc {
|
||||
pub fn new(runtime: Runtime, zones: ZoneStore) -> Self {
|
||||
Self { runtime, zones }
|
||||
}
|
||||
|
||||
pub async fn process(
|
||||
self,
|
||||
request: UpdateZoneResourcesRequest,
|
||||
) -> Result<UpdateZoneResourcesReply> {
|
||||
let uuid = Uuid::from_str(&request.zone_id)?;
|
||||
let Some(mut zone) = self.zones.read(uuid).await? else {
|
||||
return Err(anyhow!("zone not found"));
|
||||
};
|
||||
|
||||
let Some(ref mut status) = zone.status else {
|
||||
return Err(anyhow!("zone state not available"));
|
||||
};
|
||||
|
||||
if status.state() != ZoneState::Created {
|
||||
return Err(anyhow!("zone is in an invalid state"));
|
||||
}
|
||||
|
||||
if status.domid == 0 || status.domid == u32::MAX {
|
||||
return Err(anyhow!("zone domid is invalid"));
|
||||
}
|
||||
|
||||
let mut resources = request.resources.unwrap_or_default();
|
||||
if resources.target_memory > resources.max_memory {
|
||||
resources.max_memory = resources.target_memory;
|
||||
}
|
||||
|
||||
if resources.target_cpus < 1 {
|
||||
resources.target_cpus = 1;
|
||||
}
|
||||
|
||||
let initial_resources = zone
|
||||
.spec
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.initial_resources
|
||||
.unwrap_or_default();
|
||||
if resources.target_cpus > initial_resources.max_cpus {
|
||||
resources.target_cpus = initial_resources.max_cpus;
|
||||
}
|
||||
resources.max_cpus = initial_resources.max_cpus;
|
||||
|
||||
self.runtime
|
||||
.set_memory_resources(
|
||||
status.domid,
|
||||
resources.target_memory * 1024 * 1024,
|
||||
resources.max_memory * 1024 * 1024,
|
||||
)
|
||||
.await
|
||||
.map_err(|error| anyhow!("failed to set memory resources: {}", error))?;
|
||||
self.runtime
|
||||
.set_cpu_resources(status.domid, resources.target_cpus)
|
||||
.await
|
||||
.map_err(|error| anyhow!("failed to set cpu resources: {}", error))?;
|
||||
status.resource_status = Some(ZoneResourceStatus {
|
||||
active_resources: Some(resources),
|
||||
});
|
||||
|
||||
self.zones.update(uuid, zone).await?;
|
||||
Ok(UpdateZoneResourcesReply {})
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
use crate::event::DaemonEventContext;
|
||||
use anyhow::Result;
|
||||
use async_stream::try_stream;
|
||||
use krata::v1::control::{WatchEventsReply, WatchEventsRequest};
|
||||
use std::pin::Pin;
|
||||
use tokio_stream::Stream;
|
||||
use tonic::Status;
|
||||
|
||||
pub struct WatchEventsRpc {
|
||||
events: DaemonEventContext,
|
||||
}
|
||||
|
||||
impl WatchEventsRpc {
|
||||
pub fn new(events: DaemonEventContext) -> Self {
|
||||
Self { events }
|
||||
}
|
||||
|
||||
pub async fn process(
|
||||
self,
|
||||
_request: WatchEventsRequest,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = Result<WatchEventsReply, Status>> + Send + 'static>>>
|
||||
{
|
||||
let mut events = self.events.subscribe();
|
||||
let output = try_stream! {
|
||||
while let Ok(event) = events.recv().await {
|
||||
yield WatchEventsReply { event: Some(event), };
|
||||
}
|
||||
};
|
||||
Ok(Box::pin(output))
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use redb::Database;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub mod network;
|
||||
pub mod zone;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct KrataDatabase {
|
||||
pub database: Arc<Database>,
|
||||
}
|
||||
|
||||
impl KrataDatabase {
|
||||
pub fn open(path: &Path) -> Result<Self> {
|
||||
let database = Database::create(path)?;
|
||||
Ok(KrataDatabase {
|
||||
database: Arc::new(database),
|
||||
})
|
||||
}
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
use crate::db::KrataDatabase;
|
||||
use advmac::MacAddr6;
|
||||
use anyhow::Result;
|
||||
use krata::v1::common::NetworkReservation as ApiNetworkReservation;
|
||||
use log::error;
|
||||
use redb::{ReadableTable, TableDefinition};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
use uuid::Uuid;
|
||||
|
||||
const NETWORK_RESERVATION_TABLE: TableDefinition<u128, &[u8]> =
|
||||
TableDefinition::new("network-reservation");
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NetworkReservationStore {
|
||||
db: KrataDatabase,
|
||||
}
|
||||
|
||||
impl NetworkReservationStore {
|
||||
pub fn open(db: KrataDatabase) -> Result<Self> {
|
||||
let write = db.database.begin_write()?;
|
||||
let _ = write.open_table(NETWORK_RESERVATION_TABLE);
|
||||
write.commit()?;
|
||||
Ok(NetworkReservationStore { db })
|
||||
}
|
||||
|
||||
pub async fn read(&self, id: Uuid) -> Result<Option<NetworkReservation>> {
|
||||
let read = self.db.database.begin_read()?;
|
||||
let table = read.open_table(NETWORK_RESERVATION_TABLE)?;
|
||||
let Some(entry) = table.get(id.to_u128_le())? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let bytes = entry.value();
|
||||
Ok(Some(serde_json::from_slice(bytes)?))
|
||||
}
|
||||
|
||||
pub async fn list(&self) -> Result<HashMap<Uuid, NetworkReservation>> {
|
||||
enum ListEntry {
|
||||
Valid(Uuid, NetworkReservation),
|
||||
Invalid(Uuid),
|
||||
}
|
||||
let mut reservations: HashMap<Uuid, NetworkReservation> = HashMap::new();
|
||||
|
||||
let corruptions = {
|
||||
let read = self.db.database.begin_read()?;
|
||||
let table = read.open_table(NETWORK_RESERVATION_TABLE)?;
|
||||
table
|
||||
.iter()?
|
||||
.flat_map(|result| {
|
||||
result.map(|(key, value)| {
|
||||
let uuid = Uuid::from_u128_le(key.value());
|
||||
match serde_json::from_slice::<NetworkReservation>(value.value()) {
|
||||
Ok(reservation) => ListEntry::Valid(uuid, reservation),
|
||||
Err(error) => {
|
||||
error!(
|
||||
"found invalid network reservation in database for uuid {}: {}",
|
||||
uuid, error
|
||||
);
|
||||
ListEntry::Invalid(uuid)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.filter_map(|entry| match entry {
|
||||
ListEntry::Valid(uuid, reservation) => {
|
||||
reservations.insert(uuid, reservation);
|
||||
None
|
||||
}
|
||||
|
||||
ListEntry::Invalid(uuid) => Some(uuid),
|
||||
})
|
||||
.collect::<Vec<Uuid>>()
|
||||
};
|
||||
|
||||
if !corruptions.is_empty() {
|
||||
let write = self.db.database.begin_write()?;
|
||||
let mut table = write.open_table(NETWORK_RESERVATION_TABLE)?;
|
||||
for corruption in corruptions {
|
||||
table.remove(corruption.to_u128_le())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(reservations)
|
||||
}
|
||||
|
||||
pub async fn update(&self, id: Uuid, entry: NetworkReservation) -> Result<()> {
|
||||
let write = self.db.database.begin_write()?;
|
||||
{
|
||||
let mut table = write.open_table(NETWORK_RESERVATION_TABLE)?;
|
||||
let bytes = serde_json::to_vec(&entry)?;
|
||||
table.insert(id.to_u128_le(), bytes.as_slice())?;
|
||||
}
|
||||
write.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove(&self, id: Uuid) -> Result<()> {
|
||||
let write = self.db.database.begin_write()?;
|
||||
{
|
||||
let mut table = write.open_table(NETWORK_RESERVATION_TABLE)?;
|
||||
table.remove(id.to_u128_le())?;
|
||||
}
|
||||
write.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct NetworkReservation {
|
||||
pub uuid: String,
|
||||
pub ipv4: Ipv4Addr,
|
||||
pub ipv6: Ipv6Addr,
|
||||
pub mac: MacAddr6,
|
||||
pub ipv4_prefix: u8,
|
||||
pub ipv6_prefix: u8,
|
||||
pub gateway_ipv4: Ipv4Addr,
|
||||
pub gateway_ipv6: Ipv6Addr,
|
||||
pub gateway_mac: MacAddr6,
|
||||
}
|
||||
|
||||
impl From<NetworkReservation> for ApiNetworkReservation {
|
||||
fn from(val: NetworkReservation) -> Self {
|
||||
ApiNetworkReservation {
|
||||
uuid: val.uuid,
|
||||
ipv4: format!("{}/{}", val.ipv4, val.ipv4_prefix),
|
||||
ipv6: format!("{}/{}", val.ipv6, val.ipv6_prefix),
|
||||
mac: val.mac.to_string().to_lowercase().replace('-', ":"),
|
||||
gateway_ipv4: format!("{}/{}", val.gateway_ipv4, val.ipv4_prefix),
|
||||
gateway_ipv6: format!("{}/{}", val.gateway_ipv6, val.ipv6_prefix),
|
||||
gateway_mac: val.gateway_mac.to_string().to_lowercase().replace('-', ":"),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::db::KrataDatabase;
|
||||
use anyhow::Result;
|
||||
use krata::v1::common::Zone;
|
||||
use log::error;
|
||||
use prost::Message;
|
||||
use redb::{ReadableTable, TableDefinition};
|
||||
use uuid::Uuid;
|
||||
|
||||
const ZONE_TABLE: TableDefinition<u128, &[u8]> = TableDefinition::new("zone");
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ZoneStore {
|
||||
db: KrataDatabase,
|
||||
}
|
||||
|
||||
impl ZoneStore {
|
||||
pub fn open(db: KrataDatabase) -> Result<Self> {
|
||||
let write = db.database.begin_write()?;
|
||||
let _ = write.open_table(ZONE_TABLE);
|
||||
write.commit()?;
|
||||
Ok(ZoneStore { db })
|
||||
}
|
||||
|
||||
pub async fn read(&self, id: Uuid) -> Result<Option<Zone>> {
|
||||
let read = self.db.database.begin_read()?;
|
||||
let table = read.open_table(ZONE_TABLE)?;
|
||||
let Some(entry) = table.get(id.to_u128_le())? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let bytes = entry.value();
|
||||
Ok(Some(Zone::decode(bytes)?))
|
||||
}
|
||||
|
||||
pub async fn list(&self) -> Result<HashMap<Uuid, Zone>> {
|
||||
let mut zones: HashMap<Uuid, Zone> = HashMap::new();
|
||||
let read = self.db.database.begin_read()?;
|
||||
let table = read.open_table(ZONE_TABLE)?;
|
||||
for result in table.iter()? {
|
||||
let (key, value) = result?;
|
||||
let uuid = Uuid::from_u128_le(key.value());
|
||||
let state = match Zone::decode(value.value()) {
|
||||
Ok(state) => state,
|
||||
Err(error) => {
|
||||
error!(
|
||||
"found invalid zone state in database for uuid {}: {}",
|
||||
uuid, error
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
zones.insert(uuid, state);
|
||||
}
|
||||
Ok(zones)
|
||||
}
|
||||
|
||||
pub async fn update(&self, id: Uuid, entry: Zone) -> Result<()> {
|
||||
let write = self.db.database.begin_write()?;
|
||||
{
|
||||
let mut table = write.open_table(ZONE_TABLE)?;
|
||||
let bytes = entry.encode_to_vec();
|
||||
table.insert(id.to_u128_le(), bytes.as_slice())?;
|
||||
}
|
||||
write.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove(&self, id: Uuid) -> Result<()> {
|
||||
let write = self.db.database.begin_write()?;
|
||||
{
|
||||
let mut table = write.open_table(ZONE_TABLE)?;
|
||||
table.remove(id.to_u128_le())?;
|
||||
}
|
||||
write.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use log::warn;
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::config::{DaemonConfig, DaemonPciDeviceConfig};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DaemonDeviceState {
|
||||
pub pci: Option<DaemonPciDeviceConfig>,
|
||||
pub owner: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DaemonDeviceManager {
|
||||
config: Arc<DaemonConfig>,
|
||||
devices: Arc<RwLock<HashMap<String, DaemonDeviceState>>>,
|
||||
}
|
||||
|
||||
impl DaemonDeviceManager {
|
||||
pub fn new(config: Arc<DaemonConfig>) -> Self {
|
||||
Self {
|
||||
config,
|
||||
devices: Arc::new(RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn claim(&self, device: &str, uuid: Uuid) -> Result<DaemonDeviceState> {
|
||||
let mut devices = self.devices.write().await;
|
||||
let Some(state) = devices.get_mut(device) else {
|
||||
return Err(anyhow!(
|
||||
"unable to claim unknown device '{}' for zone {}",
|
||||
device,
|
||||
uuid
|
||||
));
|
||||
};
|
||||
|
||||
if let Some(owner) = state.owner {
|
||||
return Err(anyhow!(
|
||||
"unable to claim device '{}' for zone {}: already claimed by {}",
|
||||
device,
|
||||
uuid,
|
||||
owner
|
||||
));
|
||||
}
|
||||
|
||||
state.owner = Some(uuid);
|
||||
Ok(state.clone())
|
||||
}
|
||||
|
||||
pub async fn release_all(&self, uuid: Uuid) -> Result<()> {
|
||||
let mut devices = self.devices.write().await;
|
||||
for state in (*devices).values_mut() {
|
||||
if state.owner == Some(uuid) {
|
||||
state.owner = None;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn release(&self, device: &str, uuid: Uuid) -> Result<()> {
|
||||
let mut devices = self.devices.write().await;
|
||||
let Some(state) = devices.get_mut(device) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if let Some(owner) = state.owner {
|
||||
if owner != uuid {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
state.owner = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_claims(&self, claims: HashMap<String, Uuid>) -> Result<()> {
|
||||
let mut devices = self.devices.write().await;
|
||||
devices.clear();
|
||||
for (name, pci) in &self.config.pci.devices {
|
||||
let owner = claims.get(name).cloned();
|
||||
devices.insert(
|
||||
name.clone(),
|
||||
DaemonDeviceState {
|
||||
owner,
|
||||
pci: Some(pci.clone()),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
for (name, uuid) in &claims {
|
||||
if !devices.contains_key(name) {
|
||||
warn!("unknown device '{}' assigned to zone {}", name, uuid);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn copy(&self) -> Result<HashMap<String, DaemonDeviceState>> {
|
||||
let devices = self.devices.read().await;
|
||||
Ok(devices.clone())
|
||||
}
|
||||
}
|
@ -1,185 +0,0 @@
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashMap},
|
||||
str::FromStr,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::db::zone::ZoneStore;
|
||||
use crate::idm::DaemonIdmHandle;
|
||||
use anyhow::Result;
|
||||
use krata::v1::common::ZoneExitStatus;
|
||||
use krata::{
|
||||
idm::{internal::event::Event as EventType, internal::Event},
|
||||
v1::common::{ZoneState, ZoneStatus},
|
||||
};
|
||||
use log::{error, warn};
|
||||
use tokio::{
|
||||
select,
|
||||
sync::{
|
||||
broadcast,
|
||||
mpsc::{channel, Receiver, Sender},
|
||||
},
|
||||
task::JoinHandle,
|
||||
time,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub type DaemonEvent = krata::v1::control::watch_events_reply::Event;
|
||||
|
||||
const EVENT_CHANNEL_QUEUE_LEN: usize = 1000;
|
||||
const IDM_EVENT_CHANNEL_QUEUE_LEN: usize = 1000;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DaemonEventContext {
|
||||
sender: broadcast::Sender<DaemonEvent>,
|
||||
}
|
||||
|
||||
impl DaemonEventContext {
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<DaemonEvent> {
|
||||
self.sender.subscribe()
|
||||
}
|
||||
|
||||
pub fn send(&self, event: DaemonEvent) -> Result<()> {
|
||||
let _ = self.sender.send(event);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DaemonEventGenerator {
|
||||
zones: ZoneStore,
|
||||
zone_reconciler_notify: Sender<Uuid>,
|
||||
feed: broadcast::Receiver<DaemonEvent>,
|
||||
idm: DaemonIdmHandle,
|
||||
idms: HashMap<u32, (Uuid, JoinHandle<()>)>,
|
||||
idm_sender: Sender<(u32, Event)>,
|
||||
idm_receiver: Receiver<(u32, Event)>,
|
||||
_event_sender: broadcast::Sender<DaemonEvent>,
|
||||
}
|
||||
|
||||
impl DaemonEventGenerator {
|
||||
pub async fn new(
|
||||
zones: ZoneStore,
|
||||
zone_reconciler_notify: Sender<Uuid>,
|
||||
idm: DaemonIdmHandle,
|
||||
) -> Result<(DaemonEventContext, DaemonEventGenerator)> {
|
||||
let (sender, _) = broadcast::channel(EVENT_CHANNEL_QUEUE_LEN);
|
||||
let (idm_sender, idm_receiver) = channel(IDM_EVENT_CHANNEL_QUEUE_LEN);
|
||||
let generator = DaemonEventGenerator {
|
||||
zones,
|
||||
zone_reconciler_notify,
|
||||
feed: sender.subscribe(),
|
||||
idm,
|
||||
idms: HashMap::new(),
|
||||
idm_sender,
|
||||
idm_receiver,
|
||||
_event_sender: sender.clone(),
|
||||
};
|
||||
let context = DaemonEventContext { sender };
|
||||
Ok((context, generator))
|
||||
}
|
||||
|
||||
async fn handle_feed_event(&mut self, event: &DaemonEvent) -> Result<()> {
|
||||
let DaemonEvent::ZoneChanged(changed) = event;
|
||||
let Some(ref zone) = changed.zone else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(ref status) = zone.status else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let state = status.state();
|
||||
let id = Uuid::from_str(&zone.id)?;
|
||||
let domid = status.domid;
|
||||
match state {
|
||||
ZoneState::Created => {
|
||||
if let Entry::Vacant(e) = self.idms.entry(domid) {
|
||||
let client = self.idm.client_by_domid(domid).await?;
|
||||
let mut receiver = client.subscribe().await?;
|
||||
let sender = self.idm_sender.clone();
|
||||
let task = tokio::task::spawn(async move {
|
||||
loop {
|
||||
let Ok(event) = receiver.recv().await else {
|
||||
break;
|
||||
};
|
||||
|
||||
if let Err(error) = sender.send((domid, event)).await {
|
||||
warn!("unable to deliver idm event: {}", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
e.insert((id, task));
|
||||
}
|
||||
}
|
||||
|
||||
ZoneState::Destroyed => {
|
||||
if let Some((_, handle)) = self.idms.remove(&domid) {
|
||||
handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_idm_event(&mut self, id: Uuid, event: Event) -> Result<()> {
|
||||
match event.event {
|
||||
Some(EventType::Exit(exit)) => self.handle_exit_code(id, exit.code).await,
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_exit_code(&mut self, id: Uuid, code: i32) -> Result<()> {
|
||||
if let Some(mut zone) = self.zones.read(id).await? {
|
||||
zone.status = Some(ZoneStatus {
|
||||
state: ZoneState::Exited.into(),
|
||||
network_status: zone.status.clone().unwrap_or_default().network_status,
|
||||
exit_status: Some(ZoneExitStatus { code }),
|
||||
error_status: None,
|
||||
resource_status: zone.status.clone().unwrap_or_default().resource_status,
|
||||
host: zone.status.clone().map(|x| x.host).unwrap_or_default(),
|
||||
domid: zone.status.clone().map(|x| x.domid).unwrap_or(u32::MAX),
|
||||
});
|
||||
|
||||
self.zones.update(id, zone).await?;
|
||||
self.zone_reconciler_notify.send(id).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn evaluate(&mut self) -> Result<()> {
|
||||
select! {
|
||||
x = self.idm_receiver.recv() => match x {
|
||||
Some((domid, event)) => {
|
||||
if let Some((id, _)) = self.idms.get(&domid) {
|
||||
self.handle_idm_event(*id, event).await?;
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
None => {
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
x = self.feed.recv() => match x {
|
||||
Ok(event) => {
|
||||
self.handle_feed_event(&event).await
|
||||
},
|
||||
Err(error) => {
|
||||
Err(error.into())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn launch(mut self) -> Result<JoinHandle<()>> {
|
||||
Ok(tokio::task::spawn(async move {
|
||||
loop {
|
||||
if let Err(error) = self.evaluate().await {
|
||||
error!("failed to evaluate daemon events: {}", error);
|
||||
time::sleep(Duration::from_secs(5)).await;
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
@ -1,305 +0,0 @@
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashMap},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use bytes::{Buf, BytesMut};
|
||||
use krata::idm::{
|
||||
client::{IdmBackend, IdmInternalClient},
|
||||
internal::INTERNAL_IDM_CHANNEL,
|
||||
transport::IdmTransportPacket,
|
||||
};
|
||||
use kratart::channel::ChannelService;
|
||||
use log::{debug, error, warn};
|
||||
use prost::Message;
|
||||
use tokio::{
|
||||
select,
|
||||
sync::{
|
||||
broadcast,
|
||||
mpsc::{channel, Receiver, Sender},
|
||||
Mutex,
|
||||
},
|
||||
task::JoinHandle,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::zlt::ZoneLookupTable;
|
||||
|
||||
type BackendFeedMap = Arc<Mutex<HashMap<u32, Sender<IdmTransportPacket>>>>;
|
||||
type ClientMap = Arc<Mutex<HashMap<u32, IdmInternalClient>>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DaemonIdmHandle {
|
||||
zlt: ZoneLookupTable,
|
||||
clients: ClientMap,
|
||||
feeds: BackendFeedMap,
|
||||
tx_sender: Sender<(u32, IdmTransportPacket)>,
|
||||
task: Arc<JoinHandle<()>>,
|
||||
snoop_sender: broadcast::Sender<DaemonIdmSnoopPacket>,
|
||||
}
|
||||
|
||||
impl DaemonIdmHandle {
|
||||
pub fn snoop(&self) -> broadcast::Receiver<DaemonIdmSnoopPacket> {
|
||||
self.snoop_sender.subscribe()
|
||||
}
|
||||
|
||||
pub async fn client(&self, uuid: Uuid) -> Result<IdmInternalClient> {
|
||||
let Some(domid) = self.zlt.lookup_domid_by_uuid(&uuid).await else {
|
||||
return Err(anyhow!("unable to find domain {}", uuid));
|
||||
};
|
||||
self.client_by_domid(domid).await
|
||||
}
|
||||
|
||||
pub async fn client_by_domid(&self, domid: u32) -> Result<IdmInternalClient> {
|
||||
client_or_create(domid, &self.tx_sender, &self.clients, &self.feeds).await
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DaemonIdmHandle {
|
||||
fn drop(&mut self) {
|
||||
if Arc::strong_count(&self.task) <= 1 {
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DaemonIdmSnoopPacket {
|
||||
pub from: u32,
|
||||
pub to: u32,
|
||||
pub packet: IdmTransportPacket,
|
||||
}
|
||||
|
||||
pub struct DaemonIdm {
|
||||
zlt: ZoneLookupTable,
|
||||
clients: ClientMap,
|
||||
feeds: BackendFeedMap,
|
||||
tx_sender: Sender<(u32, IdmTransportPacket)>,
|
||||
tx_raw_sender: Sender<(u32, Vec<u8>)>,
|
||||
tx_receiver: Receiver<(u32, IdmTransportPacket)>,
|
||||
rx_receiver: Receiver<(u32, Option<Vec<u8>>)>,
|
||||
snoop_sender: broadcast::Sender<DaemonIdmSnoopPacket>,
|
||||
task: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl DaemonIdm {
|
||||
pub async fn new(zlt: ZoneLookupTable) -> Result<DaemonIdm> {
|
||||
debug!("allocating channel service for idm");
|
||||
let (service, tx_raw_sender, rx_receiver) =
|
||||
ChannelService::new("krata-channel".to_string(), None).await?;
|
||||
let (tx_sender, tx_receiver) = channel(100);
|
||||
let (snoop_sender, _) = broadcast::channel(100);
|
||||
|
||||
debug!("starting idm channel service");
|
||||
let task = service.launch().await?;
|
||||
|
||||
let clients = Arc::new(Mutex::new(HashMap::new()));
|
||||
let feeds = Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
Ok(DaemonIdm {
|
||||
zlt,
|
||||
rx_receiver,
|
||||
tx_receiver,
|
||||
tx_sender,
|
||||
tx_raw_sender,
|
||||
snoop_sender,
|
||||
task,
|
||||
clients,
|
||||
feeds,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn launch(mut self) -> Result<DaemonIdmHandle> {
|
||||
let zlt = self.zlt.clone();
|
||||
let clients = self.clients.clone();
|
||||
let feeds = self.feeds.clone();
|
||||
let tx_sender = self.tx_sender.clone();
|
||||
let snoop_sender = self.snoop_sender.clone();
|
||||
let task = tokio::task::spawn(async move {
|
||||
let mut buffers: HashMap<u32, BytesMut> = HashMap::new();
|
||||
|
||||
while let Err(error) = self.process(&mut buffers).await {
|
||||
error!("failed to process idm: {}", error);
|
||||
}
|
||||
});
|
||||
Ok(DaemonIdmHandle {
|
||||
zlt,
|
||||
clients,
|
||||
feeds,
|
||||
tx_sender,
|
||||
snoop_sender,
|
||||
task: Arc::new(task),
|
||||
})
|
||||
}
|
||||
|
||||
async fn process_rx_packet(
|
||||
&mut self,
|
||||
domid: u32,
|
||||
data: Option<Vec<u8>>,
|
||||
buffers: &mut HashMap<u32, BytesMut>,
|
||||
) -> Result<()> {
|
||||
// check if data is present, if it is not, that signals a closed channel.
|
||||
if let Some(data) = data {
|
||||
let buffer = buffers.entry(domid).or_insert_with_key(|_| BytesMut::new());
|
||||
buffer.extend_from_slice(&data);
|
||||
loop {
|
||||
// check if the buffer is less than the header size, if so, wait for more data
|
||||
if buffer.len() < 6 {
|
||||
break;
|
||||
}
|
||||
|
||||
// check for the magic bytes 0xff, 0xff at the start of the message, if that doesn't
|
||||
// exist, clear the buffer. this ensures that partial messages won't be processed.
|
||||
if buffer[0] != 0xff || buffer[1] != 0xff {
|
||||
buffer.clear();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// read the size from the buffer as a little endian u32
|
||||
let size = (buffer[2] as u32
|
||||
| (buffer[3] as u32) << 8
|
||||
| (buffer[4] as u32) << 16
|
||||
| (buffer[5] as u32) << 24) as usize;
|
||||
let needed = size + 6;
|
||||
if buffer.len() < needed {
|
||||
return Ok(());
|
||||
}
|
||||
let mut packet = buffer.split_to(needed);
|
||||
// advance the buffer by the header, leaving only the raw data.
|
||||
packet.advance(6);
|
||||
match IdmTransportPacket::decode(packet) {
|
||||
Ok(packet) => {
|
||||
let _ =
|
||||
client_or_create(domid, &self.tx_sender, &self.clients, &self.feeds)
|
||||
.await?;
|
||||
let guard = self.feeds.lock().await;
|
||||
if let Some(feed) = guard.get(&domid) {
|
||||
let _ = feed.try_send(packet.clone());
|
||||
}
|
||||
let _ = self.snoop_sender.send(DaemonIdmSnoopPacket {
|
||||
from: domid,
|
||||
to: 0,
|
||||
packet,
|
||||
});
|
||||
}
|
||||
|
||||
Err(packet) => {
|
||||
warn!("received invalid packet from domain {}: {}", domid, packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut clients = self.clients.lock().await;
|
||||
let mut feeds = self.feeds.lock().await;
|
||||
clients.remove(&domid);
|
||||
feeds.remove(&domid);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn tx_packet(&mut self, domid: u32, packet: IdmTransportPacket) -> Result<()> {
|
||||
let data = packet.encode_to_vec();
|
||||
let mut buffer = vec![0u8; 6];
|
||||
let length = data.len() as u32;
|
||||
// magic bytes
|
||||
buffer[0] = 0xff;
|
||||
buffer[1] = 0xff;
|
||||
// little endian u32 for message size
|
||||
buffer[2] = length as u8;
|
||||
buffer[3] = (length << 8) as u8;
|
||||
buffer[4] = (length << 16) as u8;
|
||||
buffer[5] = (length << 24) as u8;
|
||||
buffer.extend_from_slice(&data);
|
||||
self.tx_raw_sender.send((domid, buffer)).await?;
|
||||
let _ = self.snoop_sender.send(DaemonIdmSnoopPacket {
|
||||
from: 0,
|
||||
to: domid,
|
||||
packet,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process(&mut self, buffers: &mut HashMap<u32, BytesMut>) -> Result<()> {
|
||||
loop {
|
||||
select! {
|
||||
x = self.rx_receiver.recv() => match x {
|
||||
Some((domid, data)) => {
|
||||
self.process_rx_packet(domid, data, buffers).await?;
|
||||
},
|
||||
|
||||
None => {
|
||||
break;
|
||||
}
|
||||
},
|
||||
x = self.tx_receiver.recv() => match x {
|
||||
Some((domid, packet)) => {
|
||||
self.tx_packet(domid, packet).await?;
|
||||
},
|
||||
|
||||
None => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DaemonIdm {
|
||||
fn drop(&mut self) {
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
||||
|
||||
async fn client_or_create(
|
||||
domid: u32,
|
||||
tx_sender: &Sender<(u32, IdmTransportPacket)>,
|
||||
clients: &ClientMap,
|
||||
feeds: &BackendFeedMap,
|
||||
) -> Result<IdmInternalClient> {
|
||||
let mut clients = clients.lock().await;
|
||||
let mut feeds = feeds.lock().await;
|
||||
match clients.entry(domid) {
|
||||
Entry::Occupied(entry) => Ok(entry.get().clone()),
|
||||
Entry::Vacant(entry) => {
|
||||
let (rx_sender, rx_receiver) = channel(100);
|
||||
feeds.insert(domid, rx_sender);
|
||||
let backend = IdmDaemonBackend {
|
||||
domid,
|
||||
rx_receiver,
|
||||
tx_sender: tx_sender.clone(),
|
||||
};
|
||||
let client = IdmInternalClient::new(
|
||||
INTERNAL_IDM_CHANNEL,
|
||||
Box::new(backend) as Box<dyn IdmBackend>,
|
||||
)
|
||||
.await?;
|
||||
entry.insert(client.clone());
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IdmDaemonBackend {
|
||||
domid: u32,
|
||||
rx_receiver: Receiver<IdmTransportPacket>,
|
||||
tx_sender: Sender<(u32, IdmTransportPacket)>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl IdmBackend for IdmDaemonBackend {
|
||||
async fn recv(&mut self) -> Result<Vec<IdmTransportPacket>> {
|
||||
if let Some(packet) = self.rx_receiver.recv().await {
|
||||
Ok(vec![packet])
|
||||
} else {
|
||||
Err(anyhow!("idm receive channel closed"))
|
||||
}
|
||||
}
|
||||
|
||||
async fn send(&mut self, packet: IdmTransportPacket) -> Result<()> {
|
||||
self.tx_sender.send((self.domid, packet)).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,269 +0,0 @@
|
||||
use crate::db::network::NetworkReservationStore;
|
||||
use crate::db::zone::ZoneStore;
|
||||
use crate::db::KrataDatabase;
|
||||
use crate::network::assignment::NetworkAssignment;
|
||||
use anyhow::{anyhow, Result};
|
||||
use config::DaemonConfig;
|
||||
use console::{DaemonConsole, DaemonConsoleHandle};
|
||||
use control::DaemonControlService;
|
||||
use devices::DaemonDeviceManager;
|
||||
use event::{DaemonEventContext, DaemonEventGenerator};
|
||||
use idm::{DaemonIdm, DaemonIdmHandle};
|
||||
use ipnetwork::{Ipv4Network, Ipv6Network};
|
||||
use krata::{dial::ControlDialAddress, v1::control::control_service_server::ControlServiceServer};
|
||||
use krataoci::{packer::service::OciPackerService, registry::OciPlatform};
|
||||
use kratart::Runtime;
|
||||
use log::{debug, info};
|
||||
use reconcile::zone::ZoneReconciler;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use std::{net::SocketAddr, path::PathBuf, str::FromStr, sync::Arc};
|
||||
use tokio::{
|
||||
fs,
|
||||
net::UnixListener,
|
||||
sync::mpsc::{channel, Sender},
|
||||
task::JoinHandle,
|
||||
};
|
||||
use tokio_stream::wrappers::UnixListenerStream;
|
||||
use tonic::transport::{Identity, Server, ServerTlsConfig};
|
||||
use uuid::Uuid;
|
||||
use zlt::ZoneLookupTable;
|
||||
|
||||
pub mod command;
|
||||
pub mod config;
|
||||
pub mod console;
|
||||
pub mod control;
|
||||
pub mod db;
|
||||
pub mod devices;
|
||||
pub mod event;
|
||||
pub mod idm;
|
||||
pub mod metrics;
|
||||
pub mod network;
|
||||
pub mod oci;
|
||||
pub mod reconcile;
|
||||
pub mod zlt;
|
||||
|
||||
pub struct Daemon {
|
||||
store: String,
|
||||
_config: Arc<DaemonConfig>,
|
||||
zlt: ZoneLookupTable,
|
||||
devices: DaemonDeviceManager,
|
||||
zones: ZoneStore,
|
||||
network: NetworkAssignment,
|
||||
events: DaemonEventContext,
|
||||
zone_reconciler_task: JoinHandle<()>,
|
||||
zone_reconciler_notify: Sender<Uuid>,
|
||||
generator_task: JoinHandle<()>,
|
||||
idm: DaemonIdmHandle,
|
||||
console: DaemonConsoleHandle,
|
||||
packer: OciPackerService,
|
||||
runtime: Runtime,
|
||||
}
|
||||
|
||||
const ZONE_RECONCILER_QUEUE_LEN: usize = 1000;
|
||||
|
||||
impl Daemon {
|
||||
pub async fn new(store: String) -> Result<Self> {
|
||||
let store_dir = PathBuf::from(store.clone());
|
||||
debug!("loading configuration");
|
||||
let mut config_path = store_dir.clone();
|
||||
config_path.push("config.toml");
|
||||
|
||||
let config = DaemonConfig::load(&config_path).await?;
|
||||
let config = Arc::new(config);
|
||||
debug!("initializing device manager");
|
||||
let devices = DaemonDeviceManager::new(config.clone());
|
||||
|
||||
debug!("validating image cache directory");
|
||||
let mut image_cache_dir = store_dir.clone();
|
||||
image_cache_dir.push("cache");
|
||||
image_cache_dir.push("image");
|
||||
fs::create_dir_all(&image_cache_dir).await?;
|
||||
|
||||
debug!("loading zone0 uuid");
|
||||
let mut host_uuid_path = store_dir.clone();
|
||||
host_uuid_path.push("host.uuid");
|
||||
let host_uuid = if host_uuid_path.is_file() {
|
||||
let content = fs::read_to_string(&host_uuid_path).await?;
|
||||
Uuid::from_str(content.trim()).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let host_uuid = if let Some(host_uuid) = host_uuid {
|
||||
host_uuid
|
||||
} else {
|
||||
let generated = Uuid::new_v4();
|
||||
let mut string = generated.to_string();
|
||||
string.push('\n');
|
||||
fs::write(&host_uuid_path, string).await?;
|
||||
generated
|
||||
};
|
||||
|
||||
debug!("validating zone asset directories");
|
||||
let initrd_path = detect_zone_path(&store, "initrd")?;
|
||||
let kernel_path = detect_zone_path(&store, "kernel")?;
|
||||
let addons_path = detect_zone_path(&store, "addons.squashfs")?;
|
||||
|
||||
debug!("initializing caches and hydrating zone state");
|
||||
let seed = config.oci.seed.clone().map(PathBuf::from);
|
||||
let packer = OciPackerService::new(seed, &image_cache_dir, OciPlatform::current()).await?;
|
||||
debug!("initializing core runtime");
|
||||
let runtime = Runtime::new().await?;
|
||||
let zlt = ZoneLookupTable::new(0, host_uuid);
|
||||
let db_path = format!("{}/krata.db", store);
|
||||
let database = KrataDatabase::open(Path::new(&db_path))?;
|
||||
let zones = ZoneStore::open(database.clone())?;
|
||||
let (zone_reconciler_notify, zone_reconciler_receiver) =
|
||||
channel::<Uuid>(ZONE_RECONCILER_QUEUE_LEN);
|
||||
debug!("starting IDM service");
|
||||
let idm = DaemonIdm::new(zlt.clone()).await?;
|
||||
let idm = idm.launch().await?;
|
||||
debug!("initializing console interfaces");
|
||||
let console = DaemonConsole::new(zlt.clone()).await?;
|
||||
let console = console.launch().await?;
|
||||
let (events, generator) =
|
||||
DaemonEventGenerator::new(zones.clone(), zone_reconciler_notify.clone(), idm.clone())
|
||||
.await?;
|
||||
let runtime_for_reconciler = runtime.dupe().await?;
|
||||
let ipv4_network = Ipv4Network::from_str(&config.network.ipv4.subnet)?;
|
||||
let ipv6_network = Ipv6Network::from_str(&config.network.ipv6.subnet)?;
|
||||
let network_reservation_store = NetworkReservationStore::open(database)?;
|
||||
let network = NetworkAssignment::new(
|
||||
host_uuid,
|
||||
ipv4_network,
|
||||
ipv6_network,
|
||||
network_reservation_store,
|
||||
)
|
||||
.await?;
|
||||
debug!("initializing zone reconciler");
|
||||
let zone_reconciler = ZoneReconciler::new(
|
||||
devices.clone(),
|
||||
zlt.clone(),
|
||||
zones.clone(),
|
||||
events.clone(),
|
||||
runtime_for_reconciler,
|
||||
packer.clone(),
|
||||
zone_reconciler_notify.clone(),
|
||||
kernel_path,
|
||||
initrd_path,
|
||||
addons_path,
|
||||
network.clone(),
|
||||
config.clone(),
|
||||
)?;
|
||||
|
||||
let zone_reconciler_task = zone_reconciler.launch(zone_reconciler_receiver).await?;
|
||||
let generator_task = generator.launch().await?;
|
||||
|
||||
// TODO: Create a way of abstracting early init tasks in kratad.
|
||||
// TODO: Make initial power management policy configurable.
|
||||
let power = runtime.power_management_context().await?;
|
||||
power.set_smt_policy(true).await?;
|
||||
power
|
||||
.set_scheduler_policy("performance".to_string())
|
||||
.await?;
|
||||
info!("power management initialized");
|
||||
|
||||
info!("krata daemon initialized");
|
||||
Ok(Self {
|
||||
store,
|
||||
_config: config,
|
||||
zlt,
|
||||
devices,
|
||||
zones,
|
||||
network,
|
||||
events,
|
||||
zone_reconciler_task,
|
||||
zone_reconciler_notify,
|
||||
generator_task,
|
||||
idm,
|
||||
console,
|
||||
packer,
|
||||
runtime,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn listen(&mut self, addr: ControlDialAddress) -> Result<()> {
|
||||
debug!("starting control service");
|
||||
let control_service = DaemonControlService::new(
|
||||
self.zlt.clone(),
|
||||
self.devices.clone(),
|
||||
self.events.clone(),
|
||||
self.console.clone(),
|
||||
self.idm.clone(),
|
||||
self.zones.clone(),
|
||||
self.network.clone(),
|
||||
self.zone_reconciler_notify.clone(),
|
||||
self.packer.clone(),
|
||||
self.runtime.clone(),
|
||||
);
|
||||
|
||||
let mut server = Server::builder();
|
||||
|
||||
if let ControlDialAddress::Tls {
|
||||
host: _,
|
||||
port: _,
|
||||
insecure,
|
||||
} = &addr
|
||||
{
|
||||
let mut tls_config = ServerTlsConfig::new();
|
||||
if !insecure {
|
||||
let certificate_path = format!("{}/tls/daemon.pem", self.store);
|
||||
let key_path = format!("{}/tls/daemon.key", self.store);
|
||||
tls_config = tls_config.identity(Identity::from_pem(certificate_path, key_path));
|
||||
}
|
||||
server = server.tls_config(tls_config)?;
|
||||
}
|
||||
|
||||
server = server.http2_keepalive_interval(Some(Duration::from_secs(10)));
|
||||
|
||||
let server = server.add_service(ControlServiceServer::new(control_service));
|
||||
info!("listening on address {}", addr);
|
||||
match addr {
|
||||
ControlDialAddress::UnixSocket { path } => {
|
||||
let path = PathBuf::from(path);
|
||||
if path.exists() {
|
||||
fs::remove_file(&path).await?;
|
||||
}
|
||||
let listener = UnixListener::bind(path)?;
|
||||
let stream = UnixListenerStream::new(listener);
|
||||
server.serve_with_incoming(stream).await?;
|
||||
}
|
||||
|
||||
ControlDialAddress::Tcp { host, port } => {
|
||||
let address = format!("{}:{}", host, port);
|
||||
server.serve(SocketAddr::from_str(&address)?).await?;
|
||||
}
|
||||
|
||||
ControlDialAddress::Tls {
|
||||
host,
|
||||
port,
|
||||
insecure: _,
|
||||
} => {
|
||||
let address = format!("{}:{}", host, port);
|
||||
server.serve(SocketAddr::from_str(&address)?).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Daemon {
|
||||
fn drop(&mut self) {
|
||||
self.zone_reconciler_task.abort();
|
||||
self.generator_task.abort();
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_zone_path(store: &str, name: &str) -> Result<PathBuf> {
|
||||
let mut path = PathBuf::from(format!("{}/zone/{}", store, name));
|
||||
if path.is_file() {
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
path = PathBuf::from(format!("/usr/share/krata/zone/{}", name));
|
||||
if path.is_file() {
|
||||
return Ok(path);
|
||||
}
|
||||
Err(anyhow!("unable to find required zone file: {}", name))
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
use krata::{
|
||||
idm::internal::{MetricFormat, MetricNode},
|
||||
v1::common::{ZoneMetricFormat, ZoneMetricNode},
|
||||
};
|
||||
|
||||
fn idm_metric_format_to_api(format: MetricFormat) -> ZoneMetricFormat {
|
||||
match format {
|
||||
MetricFormat::Unknown => ZoneMetricFormat::Unknown,
|
||||
MetricFormat::Bytes => ZoneMetricFormat::Bytes,
|
||||
MetricFormat::Integer => ZoneMetricFormat::Integer,
|
||||
MetricFormat::DurationSeconds => ZoneMetricFormat::DurationSeconds,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn idm_metric_to_api(node: MetricNode) -> ZoneMetricNode {
|
||||
let format = node.format();
|
||||
ZoneMetricNode {
|
||||
name: node.name,
|
||||
value: node.value,
|
||||
format: idm_metric_format_to_api(format).into(),
|
||||
children: node
|
||||
.children
|
||||
.into_iter()
|
||||
.map(idm_metric_to_api)
|
||||
.collect::<Vec<_>>(),
|
||||
}
|
||||
}
|
@ -1,204 +0,0 @@
|
||||
use advmac::MacAddr6;
|
||||
use anyhow::{anyhow, Result};
|
||||
use ipnetwork::{Ipv4Network, Ipv6Network};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::{Ipv4Addr, Ipv6Addr},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::db::network::{NetworkReservation, NetworkReservationStore};
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct NetworkAssignmentState {
|
||||
pub ipv4: HashMap<Ipv4Addr, NetworkReservation>,
|
||||
pub ipv6: HashMap<Ipv6Addr, NetworkReservation>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NetworkAssignment {
|
||||
ipv4_network: Ipv4Network,
|
||||
ipv6_network: Ipv6Network,
|
||||
gateway_ipv4: Ipv4Addr,
|
||||
gateway_ipv6: Ipv6Addr,
|
||||
gateway_mac: MacAddr6,
|
||||
store: NetworkReservationStore,
|
||||
state: Arc<RwLock<NetworkAssignmentState>>,
|
||||
}
|
||||
|
||||
impl NetworkAssignment {
|
||||
pub async fn new(
|
||||
host_uuid: Uuid,
|
||||
ipv4_network: Ipv4Network,
|
||||
ipv6_network: Ipv6Network,
|
||||
store: NetworkReservationStore,
|
||||
) -> Result<Self> {
|
||||
let mut state = NetworkAssignment::fetch_current_state(&store).await?;
|
||||
let gateway_reservation = if let Some(reservation) = store.read(Uuid::nil()).await? {
|
||||
reservation
|
||||
} else {
|
||||
NetworkAssignment::allocate(
|
||||
&mut state,
|
||||
&store,
|
||||
Uuid::nil(),
|
||||
ipv4_network,
|
||||
ipv6_network,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
if store.read(host_uuid).await?.is_none() {
|
||||
let _ = NetworkAssignment::allocate(
|
||||
&mut state,
|
||||
&store,
|
||||
host_uuid,
|
||||
ipv4_network,
|
||||
ipv6_network,
|
||||
Some(gateway_reservation.gateway_ipv4),
|
||||
Some(gateway_reservation.gateway_ipv6),
|
||||
Some(gateway_reservation.gateway_mac),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let assignment = NetworkAssignment {
|
||||
ipv4_network,
|
||||
ipv6_network,
|
||||
gateway_ipv4: gateway_reservation.ipv4,
|
||||
gateway_ipv6: gateway_reservation.ipv6,
|
||||
gateway_mac: gateway_reservation.mac,
|
||||
store,
|
||||
state: Arc::new(RwLock::new(state)),
|
||||
};
|
||||
Ok(assignment)
|
||||
}
|
||||
|
||||
async fn fetch_current_state(
|
||||
store: &NetworkReservationStore,
|
||||
) -> Result<NetworkAssignmentState> {
|
||||
let reservations = store.list().await?;
|
||||
let mut state = NetworkAssignmentState::default();
|
||||
for reservation in reservations.values() {
|
||||
state.ipv4.insert(reservation.ipv4, reservation.clone());
|
||||
state.ipv6.insert(reservation.ipv6, reservation.clone());
|
||||
}
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn allocate(
|
||||
state: &mut NetworkAssignmentState,
|
||||
store: &NetworkReservationStore,
|
||||
uuid: Uuid,
|
||||
ipv4_network: Ipv4Network,
|
||||
ipv6_network: Ipv6Network,
|
||||
gateway_ipv4: Option<Ipv4Addr>,
|
||||
gateway_ipv6: Option<Ipv6Addr>,
|
||||
gateway_mac: Option<MacAddr6>,
|
||||
) -> Result<NetworkReservation> {
|
||||
let found_ipv4: Option<Ipv4Addr> = ipv4_network
|
||||
.iter()
|
||||
.filter(|ip| {
|
||||
ip.is_private() && !(ip.is_loopback() || ip.is_multicast() || ip.is_broadcast())
|
||||
})
|
||||
.filter(|ip| {
|
||||
let last = ip.octets()[3];
|
||||
// filter for IPs ending in .1 to .250 because .250+ can have special meaning
|
||||
(1..250).contains(&last)
|
||||
})
|
||||
.find(|ip| !state.ipv4.contains_key(ip));
|
||||
|
||||
let found_ipv6: Option<Ipv6Addr> = ipv6_network
|
||||
.iter()
|
||||
.filter(|ip| !ip.is_loopback() && !ip.is_multicast())
|
||||
.filter(|ip| {
|
||||
let last = ip.octets()[15];
|
||||
last > 0
|
||||
})
|
||||
.find(|ip| !state.ipv6.contains_key(ip));
|
||||
|
||||
let Some(ipv4) = found_ipv4 else {
|
||||
return Err(anyhow!(
|
||||
"unable to allocate ipv4 address, assigned network is exhausted"
|
||||
));
|
||||
};
|
||||
|
||||
let Some(ipv6) = found_ipv6 else {
|
||||
return Err(anyhow!(
|
||||
"unable to allocate ipv6 address, assigned network is exhausted"
|
||||
));
|
||||
};
|
||||
|
||||
let mut mac = MacAddr6::random();
|
||||
mac.set_local(true);
|
||||
mac.set_multicast(false);
|
||||
|
||||
let reservation = NetworkReservation {
|
||||
uuid: uuid.to_string(),
|
||||
ipv4,
|
||||
ipv6,
|
||||
mac,
|
||||
ipv4_prefix: ipv4_network.prefix(),
|
||||
ipv6_prefix: ipv6_network.prefix(),
|
||||
gateway_ipv4: gateway_ipv4.unwrap_or(ipv4),
|
||||
gateway_ipv6: gateway_ipv6.unwrap_or(ipv6),
|
||||
gateway_mac: gateway_mac.unwrap_or(mac),
|
||||
};
|
||||
state.ipv4.insert(ipv4, reservation.clone());
|
||||
state.ipv6.insert(ipv6, reservation.clone());
|
||||
store.update(uuid, reservation.clone()).await?;
|
||||
Ok(reservation)
|
||||
}
|
||||
|
||||
pub async fn assign(&self, uuid: Uuid) -> Result<NetworkReservation> {
|
||||
let mut state = self.state.write().await;
|
||||
let reservation = NetworkAssignment::allocate(
|
||||
&mut state,
|
||||
&self.store,
|
||||
uuid,
|
||||
self.ipv4_network,
|
||||
self.ipv6_network,
|
||||
Some(self.gateway_ipv4),
|
||||
Some(self.gateway_ipv6),
|
||||
Some(self.gateway_mac),
|
||||
)
|
||||
.await?;
|
||||
Ok(reservation)
|
||||
}
|
||||
|
||||
pub async fn recall(&self, uuid: Uuid) -> Result<()> {
|
||||
let mut state = self.state.write().await;
|
||||
self.store.remove(uuid).await?;
|
||||
state
|
||||
.ipv4
|
||||
.retain(|_, reservation| reservation.uuid != uuid.to_string());
|
||||
state
|
||||
.ipv6
|
||||
.retain(|_, reservation| reservation.uuid != uuid.to_string());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn retrieve(&self, uuid: Uuid) -> Result<Option<NetworkReservation>> {
|
||||
self.store.read(uuid).await
|
||||
}
|
||||
|
||||
pub async fn reload(&self) -> Result<()> {
|
||||
let mut state = self.state.write().await;
|
||||
let intermediate = NetworkAssignment::fetch_current_state(&self.store).await?;
|
||||
*state = intermediate;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn read(&self) -> Result<NetworkAssignmentState> {
|
||||
Ok(self.state.read().await.clone())
|
||||
}
|
||||
|
||||
pub async fn read_reservations(&self) -> Result<HashMap<Uuid, NetworkReservation>> {
|
||||
self.store.list().await
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
pub mod assignment;
|
@ -1,79 +0,0 @@
|
||||
use krata::v1::control::{
|
||||
image_progress_indication::Indication, ImageProgress, ImageProgressIndication,
|
||||
ImageProgressIndicationBar, ImageProgressIndicationCompleted, ImageProgressIndicationHidden,
|
||||
ImageProgressIndicationSpinner, ImageProgressLayer, ImageProgressLayerPhase,
|
||||
ImageProgressPhase,
|
||||
};
|
||||
use krataoci::progress::{
|
||||
OciProgress, OciProgressIndication, OciProgressLayer, OciProgressLayerPhase, OciProgressPhase,
|
||||
};
|
||||
|
||||
fn convert_oci_progress_indication(indication: OciProgressIndication) -> ImageProgressIndication {
|
||||
ImageProgressIndication {
|
||||
indication: Some(match indication {
|
||||
OciProgressIndication::Hidden => Indication::Hidden(ImageProgressIndicationHidden {}),
|
||||
OciProgressIndication::ProgressBar {
|
||||
message,
|
||||
current,
|
||||
total,
|
||||
bytes,
|
||||
} => Indication::Bar(ImageProgressIndicationBar {
|
||||
message: message.unwrap_or_default(),
|
||||
current,
|
||||
total,
|
||||
is_bytes: bytes,
|
||||
}),
|
||||
OciProgressIndication::Spinner { message } => {
|
||||
Indication::Spinner(ImageProgressIndicationSpinner {
|
||||
message: message.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
OciProgressIndication::Completed {
|
||||
message,
|
||||
total,
|
||||
bytes,
|
||||
} => Indication::Completed(ImageProgressIndicationCompleted {
|
||||
message: message.unwrap_or_default(),
|
||||
total: total.unwrap_or(0),
|
||||
is_bytes: bytes,
|
||||
}),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_oci_layer_progress(layer: OciProgressLayer) -> ImageProgressLayer {
|
||||
ImageProgressLayer {
|
||||
id: layer.id,
|
||||
phase: match layer.phase {
|
||||
OciProgressLayerPhase::Waiting => ImageProgressLayerPhase::Waiting,
|
||||
OciProgressLayerPhase::Downloading => ImageProgressLayerPhase::Downloading,
|
||||
OciProgressLayerPhase::Downloaded => ImageProgressLayerPhase::Downloaded,
|
||||
OciProgressLayerPhase::Extracting => ImageProgressLayerPhase::Extracting,
|
||||
OciProgressLayerPhase::Extracted => ImageProgressLayerPhase::Extracted,
|
||||
}
|
||||
.into(),
|
||||
indication: Some(convert_oci_progress_indication(layer.indication)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convert_oci_progress(oci: OciProgress) -> ImageProgress {
|
||||
ImageProgress {
|
||||
phase: match oci.phase {
|
||||
OciProgressPhase::Started => ImageProgressPhase::Started,
|
||||
OciProgressPhase::Resolving => ImageProgressPhase::Resolving,
|
||||
OciProgressPhase::Resolved => ImageProgressPhase::Resolved,
|
||||
OciProgressPhase::ConfigDownload => ImageProgressPhase::ConfigDownload,
|
||||
OciProgressPhase::LayerDownload => ImageProgressPhase::LayerDownload,
|
||||
OciProgressPhase::Assemble => ImageProgressPhase::Assemble,
|
||||
OciProgressPhase::Pack => ImageProgressPhase::Pack,
|
||||
OciProgressPhase::Complete => ImageProgressPhase::Complete,
|
||||
}
|
||||
.into(),
|
||||
layers: oci
|
||||
.layers
|
||||
.into_values()
|
||||
.map(convert_oci_layer_progress)
|
||||
.collect::<Vec<_>>(),
|
||||
indication: Some(convert_oci_progress_indication(oci.indication)),
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
pub mod zone;
|
@ -1,259 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::StreamExt;
|
||||
use krata::launchcfg::LaunchPackedFormat;
|
||||
use krata::v1::common::{OciImageFormat, Zone, ZoneState, ZoneStatus};
|
||||
use krata::v1::common::{ZoneOciImageSpec, ZoneResourceStatus};
|
||||
use krataoci::packer::{service::OciPackerService, OciPackedFormat};
|
||||
use kratart::launch::{PciBdf, PciDevice, PciRdmReservePolicy, ZoneLaunchNetwork};
|
||||
use kratart::{launch::ZoneLaunchRequest, Runtime};
|
||||
use log::info;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use crate::config::{DaemonConfig, DaemonPciDeviceRdmReservePolicy};
|
||||
use crate::devices::DaemonDeviceManager;
|
||||
use crate::network::assignment::NetworkAssignment;
|
||||
use crate::reconcile::zone::network_reservation_to_network_status;
|
||||
use crate::{reconcile::zone::ZoneReconcilerResult, zlt::ZoneLookupTable};
|
||||
use krata::v1::common::zone_image_spec::Image;
|
||||
use tokio::fs::{self, File};
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio_tar::Archive;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct ZoneCreator<'a> {
|
||||
pub devices: &'a DaemonDeviceManager,
|
||||
pub kernel_path: &'a Path,
|
||||
pub initrd_path: &'a Path,
|
||||
pub addons_path: &'a Path,
|
||||
pub packer: &'a OciPackerService,
|
||||
pub network_assignment: &'a NetworkAssignment,
|
||||
pub zlt: &'a ZoneLookupTable,
|
||||
pub runtime: &'a Runtime,
|
||||
pub config: &'a DaemonConfig,
|
||||
}
|
||||
|
||||
impl ZoneCreator<'_> {
|
||||
pub async fn oci_spec_tar_read_file(
|
||||
&self,
|
||||
file: &Path,
|
||||
oci: &ZoneOciImageSpec,
|
||||
) -> Result<Vec<u8>> {
|
||||
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 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 create(&self, uuid: Uuid, zone: &mut Zone) -> Result<ZoneReconcilerResult> {
|
||||
let Some(ref mut spec) = zone.spec else {
|
||||
return Err(anyhow!("zone 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 zones"));
|
||||
}
|
||||
},
|
||||
)
|
||||
.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 success = AtomicBool::new(false);
|
||||
|
||||
let _device_release_guard = scopeguard::guard(
|
||||
(spec.devices.clone(), self.devices.clone()),
|
||||
|(devices, manager)| {
|
||||
if !success.load(Ordering::Acquire) {
|
||||
tokio::task::spawn(async move {
|
||||
for device in devices {
|
||||
let _ = manager.release(&device.name, uuid).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let mut pcis = Vec::new();
|
||||
for device in &spec.devices {
|
||||
let state = self.devices.claim(&device.name, uuid).await?;
|
||||
if let Some(cfg) = state.pci {
|
||||
for location in cfg.locations {
|
||||
let pci = PciDevice {
|
||||
bdf: PciBdf::from_str(&location)?.with_domain(0),
|
||||
permissive: cfg.permissive,
|
||||
msi_translate: cfg.msi_translate,
|
||||
power_management: cfg.power_management,
|
||||
rdm_reserve_policy: match cfg.rdm_reserve_policy {
|
||||
DaemonPciDeviceRdmReservePolicy::Strict => PciRdmReservePolicy::Strict,
|
||||
DaemonPciDeviceRdmReservePolicy::Relaxed => {
|
||||
PciRdmReservePolicy::Relaxed
|
||||
}
|
||||
},
|
||||
};
|
||||
pcis.push(pci);
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow!(
|
||||
"device '{}' isn't a known device type",
|
||||
device.name
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let reservation = self.network_assignment.assign(uuid).await?;
|
||||
|
||||
let mut initial_resources = spec.initial_resources.unwrap_or_default();
|
||||
if initial_resources.target_cpus < 1 {
|
||||
initial_resources.target_cpus = 1;
|
||||
}
|
||||
if initial_resources.target_cpus > initial_resources.max_cpus {
|
||||
initial_resources.max_cpus = initial_resources.target_cpus;
|
||||
}
|
||||
spec.initial_resources = Some(initial_resources);
|
||||
let kernel_options = spec.kernel_options.clone().unwrap_or_default();
|
||||
let info = self
|
||||
.runtime
|
||||
.launch(ZoneLaunchRequest {
|
||||
format: match image.format {
|
||||
OciPackedFormat::Squashfs => LaunchPackedFormat::Squashfs,
|
||||
OciPackedFormat::Erofs => LaunchPackedFormat::Erofs,
|
||||
_ => {
|
||||
return Err(anyhow!(
|
||||
"oci image is in an invalid format, which isn't compatible with launch"
|
||||
));
|
||||
}
|
||||
},
|
||||
uuid: Some(uuid),
|
||||
name: if spec.name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(spec.name.clone())
|
||||
},
|
||||
image,
|
||||
kernel,
|
||||
initrd,
|
||||
target_cpus: initial_resources.target_cpus,
|
||||
max_cpus: initial_resources.max_cpus,
|
||||
max_memory: initial_resources.max_memory,
|
||||
target_memory: initial_resources.target_memory,
|
||||
pcis,
|
||||
env: task
|
||||
.environment
|
||||
.iter()
|
||||
.map(|x| (x.key.clone(), x.value.clone()))
|
||||
.collect::<HashMap<_, _>>(),
|
||||
run: empty_vec_optional(task.command.clone()),
|
||||
kernel_verbose: kernel_options.verbose,
|
||||
kernel_cmdline_append: kernel_options.cmdline_append,
|
||||
addons_image: Some(self.addons_path.to_path_buf()),
|
||||
network: ZoneLaunchNetwork {
|
||||
ipv4: reservation.ipv4.to_string(),
|
||||
ipv4_prefix: reservation.ipv4_prefix,
|
||||
ipv6: reservation.ipv6.to_string(),
|
||||
ipv6_prefix: reservation.ipv6_prefix,
|
||||
gateway_ipv4: reservation.gateway_ipv4.to_string(),
|
||||
gateway_ipv6: reservation.gateway_ipv6.to_string(),
|
||||
zone_mac: reservation.mac,
|
||||
nameservers: self.config.network.nameservers.clone(),
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
self.zlt.associate(uuid, info.domid).await;
|
||||
info!("created zone {}", uuid);
|
||||
zone.status = Some(ZoneStatus {
|
||||
state: ZoneState::Created.into(),
|
||||
network_status: Some(network_reservation_to_network_status(&reservation)),
|
||||
exit_status: None,
|
||||
error_status: None,
|
||||
resource_status: Some(ZoneResourceStatus {
|
||||
active_resources: Some(initial_resources),
|
||||
}),
|
||||
host: self.zlt.host_uuid().to_string(),
|
||||
domid: info.domid,
|
||||
});
|
||||
success.store(true, Ordering::Release);
|
||||
Ok(ZoneReconcilerResult::Changed { rerun: false })
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_vec_optional<T>(value: Vec<T>) -> Option<Vec<T>> {
|
||||
if value.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
}
|
@ -1,381 +0,0 @@
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashMap},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use self::create::ZoneCreator;
|
||||
use crate::config::DaemonConfig;
|
||||
use crate::db::network::NetworkReservation;
|
||||
use crate::network::assignment::NetworkAssignment;
|
||||
use crate::{
|
||||
db::zone::ZoneStore,
|
||||
devices::DaemonDeviceManager,
|
||||
event::{DaemonEvent, DaemonEventContext},
|
||||
zlt::ZoneLookupTable,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use krata::v1::{
|
||||
common::{Zone, ZoneErrorStatus, ZoneExitStatus, ZoneNetworkStatus, ZoneState, ZoneStatus},
|
||||
control::ZoneChangedEvent,
|
||||
};
|
||||
use krataoci::packer::service::OciPackerService;
|
||||
use kratart::Runtime;
|
||||
use log::{error, info, trace, warn};
|
||||
use tokio::{
|
||||
select,
|
||||
sync::{
|
||||
mpsc::{channel, Receiver, Sender},
|
||||
RwLock,
|
||||
},
|
||||
task::JoinHandle,
|
||||
time::sleep,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
mod create;
|
||||
|
||||
const PARALLEL_LIMIT: u32 = 5;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ZoneReconcilerResult {
|
||||
Unchanged,
|
||||
Changed { rerun: bool },
|
||||
}
|
||||
|
||||
struct ZoneReconcilerEntry {
|
||||
sender: Sender<()>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ZoneReconciler {
|
||||
devices: DaemonDeviceManager,
|
||||
zlt: ZoneLookupTable,
|
||||
zones: ZoneStore,
|
||||
events: DaemonEventContext,
|
||||
runtime: Runtime,
|
||||
packer: OciPackerService,
|
||||
kernel_path: PathBuf,
|
||||
initrd_path: PathBuf,
|
||||
addons_path: PathBuf,
|
||||
tasks: Arc<RwLock<HashMap<Uuid, ZoneReconcilerEntry>>>,
|
||||
zone_reconciler_notify: Sender<Uuid>,
|
||||
zone_reconcile_lock: Arc<RwLock<()>>,
|
||||
ip_assignment: NetworkAssignment,
|
||||
config: Arc<DaemonConfig>,
|
||||
}
|
||||
|
||||
impl ZoneReconciler {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
devices: DaemonDeviceManager,
|
||||
zlt: ZoneLookupTable,
|
||||
zones: ZoneStore,
|
||||
events: DaemonEventContext,
|
||||
runtime: Runtime,
|
||||
packer: OciPackerService,
|
||||
zone_reconciler_notify: Sender<Uuid>,
|
||||
kernel_path: PathBuf,
|
||||
initrd_path: PathBuf,
|
||||
modules_path: PathBuf,
|
||||
ip_assignment: NetworkAssignment,
|
||||
config: Arc<DaemonConfig>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
devices,
|
||||
zlt,
|
||||
zones,
|
||||
events,
|
||||
runtime,
|
||||
packer,
|
||||
kernel_path,
|
||||
initrd_path,
|
||||
addons_path: modules_path,
|
||||
tasks: Arc::new(RwLock::new(HashMap::new())),
|
||||
zone_reconciler_notify,
|
||||
zone_reconcile_lock: Arc::new(RwLock::with_max_readers((), PARALLEL_LIMIT)),
|
||||
ip_assignment,
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn launch(self, mut notify: Receiver<Uuid>) -> Result<JoinHandle<()>> {
|
||||
Ok(tokio::task::spawn(async move {
|
||||
if let Err(error) = self.reconcile_runtime(true).await {
|
||||
error!("runtime reconciler failed: {}", error);
|
||||
}
|
||||
|
||||
loop {
|
||||
select! {
|
||||
x = notify.recv() => match x {
|
||||
None => {
|
||||
break;
|
||||
},
|
||||
|
||||
Some(uuid) => {
|
||||
if let Err(error) = self.launch_task_if_needed(uuid).await {
|
||||
error!("failed to start zone reconciler task {}: {}", uuid, error);
|
||||
}
|
||||
|
||||
let map = self.tasks.read().await;
|
||||
if let Some(entry) = map.get(&uuid) {
|
||||
if let Err(error) = entry.sender.send(()).await {
|
||||
error!("failed to notify zone reconciler task {}: {}", uuid, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_ = sleep(Duration::from_secs(15)) => {
|
||||
if let Err(error) = self.reconcile_runtime(false).await {
|
||||
error!("runtime reconciler failed: {}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn reconcile_runtime(&self, initial: bool) -> Result<()> {
|
||||
let _permit = self.zone_reconcile_lock.write().await;
|
||||
trace!("reconciling runtime");
|
||||
let runtime_zones = self.runtime.list().await?;
|
||||
let stored_zones = self.zones.list().await?;
|
||||
|
||||
let non_existent_zones = runtime_zones
|
||||
.iter()
|
||||
.filter(|x| !stored_zones.iter().any(|g| *g.0 == x.uuid))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for zone in non_existent_zones {
|
||||
warn!("destroying unknown runtime zone {}", zone.uuid);
|
||||
if let Err(error) = self.runtime.destroy(zone.uuid).await {
|
||||
error!(
|
||||
"failed to destroy unknown runtime zone {}: {}",
|
||||
zone.uuid, error
|
||||
);
|
||||
}
|
||||
self.zones.remove(zone.uuid).await?;
|
||||
}
|
||||
|
||||
let mut device_claims = HashMap::new();
|
||||
|
||||
for (uuid, mut stored_zone) in stored_zones {
|
||||
let previous_zone = stored_zone.clone();
|
||||
let runtime_zone = runtime_zones.iter().find(|x| x.uuid == uuid);
|
||||
match runtime_zone {
|
||||
None => {
|
||||
let mut status = stored_zone.status.as_mut().cloned().unwrap_or_default();
|
||||
if status.state() == ZoneState::Created {
|
||||
status.state = ZoneState::Creating.into();
|
||||
}
|
||||
stored_zone.status = Some(status);
|
||||
}
|
||||
|
||||
Some(runtime) => {
|
||||
self.zlt.associate(uuid, runtime.domid).await;
|
||||
let mut status = stored_zone.status.as_mut().cloned().unwrap_or_default();
|
||||
if let Some(code) = runtime.state.exit_code {
|
||||
status.state = ZoneState::Exited.into();
|
||||
status.exit_status = Some(ZoneExitStatus { code });
|
||||
} else {
|
||||
status.state = ZoneState::Created.into();
|
||||
}
|
||||
|
||||
for device in &stored_zone
|
||||
.spec
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.devices
|
||||
{
|
||||
device_claims.insert(device.name.clone(), uuid);
|
||||
}
|
||||
|
||||
if let Some(reservation) = self.ip_assignment.retrieve(uuid).await? {
|
||||
status.network_status =
|
||||
Some(network_reservation_to_network_status(&reservation));
|
||||
}
|
||||
stored_zone.status = Some(status);
|
||||
}
|
||||
}
|
||||
|
||||
let changed = stored_zone != previous_zone;
|
||||
|
||||
if changed || initial {
|
||||
self.zones.update(uuid, stored_zone).await?;
|
||||
let _ = self.zone_reconciler_notify.try_send(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
self.devices.update_claims(device_claims).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reconcile(&self, uuid: Uuid) -> Result<bool> {
|
||||
let _runtime_reconcile_permit = self.zone_reconcile_lock.read().await;
|
||||
let Some(mut zone) = self.zones.read(uuid).await? else {
|
||||
warn!(
|
||||
"notified of reconcile for zone {} but it didn't exist",
|
||||
uuid
|
||||
);
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
info!("reconciling zone {}", uuid);
|
||||
|
||||
self.events
|
||||
.send(DaemonEvent::ZoneChanged(ZoneChangedEvent {
|
||||
zone: Some(zone.clone()),
|
||||
}))?;
|
||||
|
||||
let start_state = zone.status.as_ref().map(|x| x.state()).unwrap_or_default();
|
||||
let result = match start_state {
|
||||
ZoneState::Creating => self.create(uuid, &mut zone).await,
|
||||
ZoneState::Exited => self.exited(&mut zone).await,
|
||||
ZoneState::Destroying => self.destroy(uuid, &mut zone).await,
|
||||
_ => Ok(ZoneReconcilerResult::Unchanged),
|
||||
};
|
||||
|
||||
let result = match result {
|
||||
Ok(result) => result,
|
||||
Err(error) => {
|
||||
zone.status = Some(zone.status.as_mut().cloned().unwrap_or_default());
|
||||
zone.status.as_mut().unwrap().state = ZoneState::Failed.into();
|
||||
zone.status.as_mut().unwrap().error_status = Some(ZoneErrorStatus {
|
||||
message: error.to_string(),
|
||||
});
|
||||
warn!("failed to start zone {}: {}", zone.id, error);
|
||||
ZoneReconcilerResult::Changed { rerun: false }
|
||||
}
|
||||
};
|
||||
|
||||
info!("reconciled zone {}", uuid);
|
||||
|
||||
let state = zone.status.as_ref().map(|x| x.state()).unwrap_or_default();
|
||||
let destroyed = state == ZoneState::Destroyed;
|
||||
|
||||
let rerun = if let ZoneReconcilerResult::Changed { rerun } = result {
|
||||
let event = DaemonEvent::ZoneChanged(ZoneChangedEvent {
|
||||
zone: Some(zone.clone()),
|
||||
});
|
||||
|
||||
if destroyed {
|
||||
self.zones.remove(uuid).await?;
|
||||
let mut map = self.tasks.write().await;
|
||||
map.remove(&uuid);
|
||||
} else {
|
||||
self.zones.update(uuid, zone.clone()).await?;
|
||||
}
|
||||
|
||||
self.events.send(event)?;
|
||||
rerun
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
Ok(rerun)
|
||||
}
|
||||
|
||||
async fn create(&self, uuid: Uuid, zone: &mut Zone) -> Result<ZoneReconcilerResult> {
|
||||
let starter = ZoneCreator {
|
||||
devices: &self.devices,
|
||||
kernel_path: &self.kernel_path,
|
||||
initrd_path: &self.initrd_path,
|
||||
addons_path: &self.addons_path,
|
||||
packer: &self.packer,
|
||||
network_assignment: &self.ip_assignment,
|
||||
zlt: &self.zlt,
|
||||
runtime: &self.runtime,
|
||||
config: &self.config,
|
||||
};
|
||||
starter.create(uuid, zone).await
|
||||
}
|
||||
|
||||
async fn exited(&self, zone: &mut Zone) -> Result<ZoneReconcilerResult> {
|
||||
if let Some(ref mut status) = zone.status {
|
||||
status.set_state(ZoneState::Destroying);
|
||||
Ok(ZoneReconcilerResult::Changed { rerun: true })
|
||||
} else {
|
||||
Ok(ZoneReconcilerResult::Unchanged)
|
||||
}
|
||||
}
|
||||
|
||||
async fn destroy(&self, uuid: Uuid, zone: &mut Zone) -> Result<ZoneReconcilerResult> {
|
||||
if let Err(error) = self.runtime.destroy(uuid).await {
|
||||
trace!("failed to destroy runtime zone {}: {}", uuid, error);
|
||||
}
|
||||
|
||||
let domid = zone.status.as_ref().map(|x| x.domid);
|
||||
|
||||
if let Some(domid) = domid {
|
||||
self.zlt.remove(uuid, domid).await;
|
||||
}
|
||||
|
||||
info!("destroyed zone {}", uuid);
|
||||
self.ip_assignment.recall(uuid).await?;
|
||||
zone.status = Some(ZoneStatus {
|
||||
state: ZoneState::Destroyed.into(),
|
||||
network_status: None,
|
||||
exit_status: None,
|
||||
error_status: None,
|
||||
resource_status: None,
|
||||
host: self.zlt.host_uuid().to_string(),
|
||||
domid: domid.unwrap_or(u32::MAX),
|
||||
});
|
||||
self.devices.release_all(uuid).await?;
|
||||
Ok(ZoneReconcilerResult::Changed { rerun: false })
|
||||
}
|
||||
|
||||
async fn launch_task_if_needed(&self, uuid: Uuid) -> Result<()> {
|
||||
let mut map = self.tasks.write().await;
|
||||
match map.entry(uuid) {
|
||||
Entry::Occupied(_) => {}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(self.launch_task(uuid).await?);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn launch_task(&self, uuid: Uuid) -> Result<ZoneReconcilerEntry> {
|
||||
let this = self.clone();
|
||||
let (sender, mut receiver) = channel(10);
|
||||
tokio::task::spawn(async move {
|
||||
'notify_loop: loop {
|
||||
if receiver.recv().await.is_none() {
|
||||
break 'notify_loop;
|
||||
}
|
||||
|
||||
'rerun_loop: loop {
|
||||
let rerun = match this.reconcile(uuid).await {
|
||||
Ok(rerun) => rerun,
|
||||
Err(error) => {
|
||||
error!("failed to reconcile zone {}: {}", uuid, error);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if rerun {
|
||||
continue 'rerun_loop;
|
||||
}
|
||||
break 'rerun_loop;
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(ZoneReconcilerEntry { sender })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn network_reservation_to_network_status(ip: &NetworkReservation) -> ZoneNetworkStatus {
|
||||
ZoneNetworkStatus {
|
||||
zone_ipv4: format!("{}/{}", ip.ipv4, ip.ipv4_prefix),
|
||||
zone_ipv6: format!("{}/{}", ip.ipv6, ip.ipv6_prefix),
|
||||
zone_mac: ip.mac.to_string().to_lowercase().replace('-', ":"),
|
||||
gateway_ipv4: format!("{}/{}", ip.gateway_ipv4, ip.ipv4_prefix),
|
||||
gateway_ipv6: format!("{}/{}", ip.gateway_ipv6, ip.ipv6_prefix),
|
||||
gateway_mac: ip.gateway_mac.to_string().to_lowercase().replace('-', ":"),
|
||||
}
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
struct ZoneLookupTableState {
|
||||
domid_to_uuid: HashMap<u32, Uuid>,
|
||||
uuid_to_domid: HashMap<Uuid, u32>,
|
||||
}
|
||||
|
||||
impl ZoneLookupTableState {
|
||||
pub fn new(host_uuid: Uuid) -> Self {
|
||||
let mut domid_to_uuid = HashMap::new();
|
||||
let mut uuid_to_domid = HashMap::new();
|
||||
domid_to_uuid.insert(0, host_uuid);
|
||||
uuid_to_domid.insert(host_uuid, 0);
|
||||
ZoneLookupTableState {
|
||||
domid_to_uuid,
|
||||
uuid_to_domid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ZoneLookupTable {
|
||||
host_domid: u32,
|
||||
host_uuid: Uuid,
|
||||
state: Arc<RwLock<ZoneLookupTableState>>,
|
||||
}
|
||||
|
||||
impl ZoneLookupTable {
|
||||
pub fn new(host_domid: u32, host_uuid: Uuid) -> Self {
|
||||
ZoneLookupTable {
|
||||
host_domid,
|
||||
host_uuid,
|
||||
state: Arc::new(RwLock::new(ZoneLookupTableState::new(host_uuid))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn host_uuid(&self) -> Uuid {
|
||||
self.host_uuid
|
||||
}
|
||||
|
||||
pub fn host_domid(&self) -> u32 {
|
||||
self.host_domid
|
||||
}
|
||||
|
||||
pub async fn lookup_uuid_by_domid(&self, domid: u32) -> Option<Uuid> {
|
||||
let state = self.state.read().await;
|
||||
state.domid_to_uuid.get(&domid).cloned()
|
||||
}
|
||||
|
||||
pub async fn lookup_domid_by_uuid(&self, uuid: &Uuid) -> Option<u32> {
|
||||
let state = self.state.read().await;
|
||||
state.uuid_to_domid.get(uuid).cloned()
|
||||
}
|
||||
|
||||
pub async fn associate(&self, uuid: Uuid, domid: u32) {
|
||||
let mut state = self.state.write().await;
|
||||
state.uuid_to_domid.insert(uuid, domid);
|
||||
state.domid_to_uuid.insert(domid, uuid);
|
||||
}
|
||||
|
||||
pub async fn remove(&self, uuid: Uuid, domid: u32) {
|
||||
let mut state = self.state.write().await;
|
||||
state.uuid_to_domid.remove(&uuid);
|
||||
state.domid_to_uuid.remove(&domid);
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
[package]
|
||||
name = "krata"
|
||||
description = "Client library and common services for the krata isolation engine"
|
||||
license.workspace = true
|
||||
version.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
log = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
pin-project-lite = { workspace = true }
|
||||
prost = { workspace = true }
|
||||
prost-reflect = { workspace = true }
|
||||
prost-types = { workspace = true }
|
||||
scopeguard = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
tonic = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
tower = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
hyper = { workspace = true }
|
||||
hyper-util = { workspace = true }
|
||||
nix = { workspace = true, features = ["term"] }
|
||||
|
||||
[build-dependencies]
|
||||
tonic-build = { workspace = true }
|
||||
prost-build = { workspace = true }
|
||||
prost-reflect-build = { workspace = true }
|
||||
|
||||
[lib]
|
||||
name = "krata"
|
||||
|
||||
[[example]]
|
||||
name = "ethtool"
|
||||
path = "examples/ethtool.rs"
|
@ -1,26 +0,0 @@
|
||||
use std::io::Result;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut config = prost_build::Config::new();
|
||||
prost_reflect_build::Builder::new()
|
||||
.descriptor_pool("crate::DESCRIPTOR_POOL")
|
||||
.configure(
|
||||
&mut config,
|
||||
&[
|
||||
"proto/krata/v1/control.proto",
|
||||
"proto/krata/idm/transport.proto",
|
||||
"proto/krata/idm/internal.proto",
|
||||
],
|
||||
&["proto/"],
|
||||
)?;
|
||||
tonic_build::configure().compile_with_config(
|
||||
config,
|
||||
&[
|
||||
"proto/krata/v1/control.proto",
|
||||
"proto/krata/idm/transport.proto",
|
||||
"proto/krata/idm/internal.proto",
|
||||
],
|
||||
&["proto/"],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
use std::env;
|
||||
|
||||
use anyhow::Result;
|
||||
use krata::ethtool::EthtoolHandle;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args = env::args().collect::<Vec<String>>();
|
||||
let interface = args.get(1).unwrap();
|
||||
let mut handle = EthtoolHandle::new()?;
|
||||
handle.set_gso(interface, false)?;
|
||||
handle.set_tso(interface, false)?;
|
||||
Ok(())
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package krata.idm.internal;
|
||||
|
||||
option java_multiple_files = true;
|
||||
option java_package = "dev.krata.proto.idm.internal";
|
||||
option java_outer_classname = "IdmInternalProto";
|
||||
|
||||
import "google/protobuf/struct.proto";
|
||||
|
||||
message ExitEvent {
|
||||
int32 code = 1;
|
||||
}
|
||||
|
||||
message PingRequest {}
|
||||
|
||||
message PingResponse {}
|
||||
|
||||
message MetricsRequest {}
|
||||
|
||||
message MetricsResponse {
|
||||
MetricNode root = 1;
|
||||
}
|
||||
|
||||
message MetricNode {
|
||||
string name = 1;
|
||||
google.protobuf.Value value = 2;
|
||||
MetricFormat format = 3;
|
||||
repeated MetricNode children = 4;
|
||||
}
|
||||
|
||||
enum MetricFormat {
|
||||
METRIC_FORMAT_UNKNOWN = 0;
|
||||
METRIC_FORMAT_BYTES = 1;
|
||||
METRIC_FORMAT_INTEGER = 2;
|
||||
METRIC_FORMAT_DURATION_SECONDS = 3;
|
||||
}
|
||||
|
||||
message ExecEnvVar {
|
||||
string key = 1;
|
||||
string value = 2;
|
||||
}
|
||||
|
||||
message ExecStreamRequestStart {
|
||||
repeated ExecEnvVar environment = 1;
|
||||
repeated string command = 2;
|
||||
string working_directory = 3;
|
||||
bool tty = 4;
|
||||
ExecStreamRequestTerminalSize terminal_size = 5;
|
||||
}
|
||||
|
||||
message ExecStreamRequestStdin {
|
||||
bytes data = 1;
|
||||
bool closed = 2;
|
||||
}
|
||||
|
||||
message ExecStreamRequestTerminalSize {
|
||||
uint32 rows = 1;
|
||||
uint32 columns = 2;
|
||||
}
|
||||
|
||||
message ExecStreamRequestUpdate {
|
||||
oneof update {
|
||||
ExecStreamRequestStart start = 1;
|
||||
ExecStreamRequestStdin stdin = 2;
|
||||
ExecStreamRequestTerminalSize terminal_resize = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message ExecStreamResponseUpdate {
|
||||
bool exited = 1;
|
||||
string error = 2;
|
||||
int32 exit_code = 3;
|
||||
bytes stdout = 4;
|
||||
bytes stderr = 5;
|
||||
}
|
||||
|
||||
message Event {
|
||||
oneof event {
|
||||
ExitEvent exit = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message Request {
|
||||
oneof request {
|
||||
PingRequest ping = 1;
|
||||
MetricsRequest metrics = 2;
|
||||
ExecStreamRequestUpdate exec_stream = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message Response {
|
||||
oneof response {
|
||||
PingResponse ping = 1;
|
||||
MetricsResponse metrics = 2;
|
||||
ExecStreamResponseUpdate exec_stream = 3;
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package krata.idm.transport;
|
||||
|
||||
option java_multiple_files = true;
|
||||
option java_package = "dev.krata.proto.idm.transport";
|
||||
option java_outer_classname = "IdmTransportProto";
|
||||
|
||||
message IdmTransportPacket {
|
||||
uint64 id = 1;
|
||||
uint64 channel = 2;
|
||||
IdmTransportPacketForm form = 3;
|
||||
bytes data = 4;
|
||||
}
|
||||
|
||||
enum IdmTransportPacketForm {
|
||||
IDM_TRANSPORT_PACKET_FORM_UNKNOWN = 0;
|
||||
IDM_TRANSPORT_PACKET_FORM_RAW = 1;
|
||||
IDM_TRANSPORT_PACKET_FORM_EVENT = 2;
|
||||
IDM_TRANSPORT_PACKET_FORM_REQUEST = 3;
|
||||
IDM_TRANSPORT_PACKET_FORM_RESPONSE = 4;
|
||||
IDM_TRANSPORT_PACKET_FORM_STREAM_REQUEST = 5;
|
||||
IDM_TRANSPORT_PACKET_FORM_STREAM_REQUEST_UPDATE = 6;
|
||||
IDM_TRANSPORT_PACKET_FORM_STREAM_RESPONSE_UPDATE = 7;
|
||||
IDM_TRANSPORT_PACKET_FORM_STREAM_REQUEST_CLOSED = 8;
|
||||
IDM_TRANSPORT_PACKET_FORM_STREAM_RESPONSE_CLOSED = 9;
|
||||
}
|
@ -1,151 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package krata.v1.common;
|
||||
|
||||
option java_multiple_files = true;
|
||||
option java_package = "dev.krata.proto.v1.common";
|
||||
option java_outer_classname = "CommonProto";
|
||||
|
||||
import "google/protobuf/struct.proto";
|
||||
|
||||
message Zone {
|
||||
string id = 1;
|
||||
ZoneSpec spec = 2;
|
||||
ZoneStatus status = 3;
|
||||
}
|
||||
|
||||
message ZoneSpec {
|
||||
string name = 1;
|
||||
ZoneImageSpec image = 2;
|
||||
// If not specified, defaults to the daemon default kernel.
|
||||
ZoneImageSpec kernel = 3;
|
||||
// If not specified, defaults to the daemon default initrd.
|
||||
ZoneImageSpec initrd = 4;
|
||||
ZoneResourceSpec initial_resources = 5;
|
||||
ZoneTaskSpec task = 6;
|
||||
repeated ZoneSpecAnnotation annotations = 7;
|
||||
repeated ZoneSpecDevice devices = 8;
|
||||
ZoneKernelOptionsSpec kernel_options = 9;
|
||||
}
|
||||
|
||||
message ZoneResourceSpec {
|
||||
uint64 max_memory = 1;
|
||||
uint64 target_memory = 2;
|
||||
uint32 max_cpus = 3;
|
||||
uint32 target_cpus = 4;
|
||||
}
|
||||
|
||||
message ZoneImageSpec {
|
||||
oneof image {
|
||||
ZoneOciImageSpec oci = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message ZoneKernelOptionsSpec {
|
||||
bool verbose = 1;
|
||||
string cmdline_append = 2;
|
||||
}
|
||||
|
||||
enum OciImageFormat {
|
||||
OCI_IMAGE_FORMAT_UNKNOWN = 0;
|
||||
OCI_IMAGE_FORMAT_SQUASHFS = 1;
|
||||
OCI_IMAGE_FORMAT_EROFS = 2;
|
||||
// Tar format is not launchable, and is intended for kernel images.
|
||||
OCI_IMAGE_FORMAT_TAR = 3;
|
||||
}
|
||||
|
||||
message ZoneOciImageSpec {
|
||||
string digest = 1;
|
||||
OciImageFormat format = 2;
|
||||
}
|
||||
|
||||
message ZoneTaskSpec {
|
||||
repeated ZoneTaskSpecEnvVar environment = 1;
|
||||
repeated string command = 2;
|
||||
string working_directory = 3;
|
||||
bool tty = 4;
|
||||
}
|
||||
|
||||
message ZoneTaskSpecEnvVar {
|
||||
string key = 1;
|
||||
string value = 2;
|
||||
}
|
||||
|
||||
message ZoneSpecAnnotation {
|
||||
string key = 1;
|
||||
string value = 2;
|
||||
}
|
||||
|
||||
message ZoneSpecDevice {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
message ZoneStatus {
|
||||
ZoneState state = 1;
|
||||
ZoneNetworkStatus network_status = 2;
|
||||
ZoneExitStatus exit_status = 3;
|
||||
ZoneErrorStatus error_status = 4;
|
||||
string host = 5;
|
||||
uint32 domid = 6;
|
||||
ZoneResourceStatus resource_status = 7;
|
||||
}
|
||||
|
||||
enum ZoneState {
|
||||
ZONE_STATE_UNKNOWN = 0;
|
||||
ZONE_STATE_CREATING = 1;
|
||||
ZONE_STATE_CREATED = 2;
|
||||
ZONE_STATE_EXITED = 3;
|
||||
ZONE_STATE_DESTROYING = 4;
|
||||
ZONE_STATE_DESTROYED = 5;
|
||||
ZONE_STATE_FAILED = 6;
|
||||
}
|
||||
|
||||
message ZoneNetworkStatus {
|
||||
string zone_ipv4 = 1;
|
||||
string zone_ipv6 = 2;
|
||||
string zone_mac = 3;
|
||||
string gateway_ipv4 = 4;
|
||||
string gateway_ipv6 = 5;
|
||||
string gateway_mac = 6;
|
||||
}
|
||||
|
||||
message ZoneExitStatus {
|
||||
int32 code = 1;
|
||||
}
|
||||
|
||||
message ZoneErrorStatus {
|
||||
string message = 1;
|
||||
}
|
||||
|
||||
message ZoneResourceStatus {
|
||||
ZoneResourceSpec active_resources = 1;
|
||||
}
|
||||
|
||||
message ZoneMetricNode {
|
||||
string name = 1;
|
||||
google.protobuf.Value value = 2;
|
||||
ZoneMetricFormat format = 3;
|
||||
repeated ZoneMetricNode children = 4;
|
||||
}
|
||||
|
||||
enum ZoneMetricFormat {
|
||||
ZONE_METRIC_FORMAT_UNKNOWN = 0;
|
||||
ZONE_METRIC_FORMAT_BYTES = 1;
|
||||
ZONE_METRIC_FORMAT_INTEGER = 2;
|
||||
ZONE_METRIC_FORMAT_DURATION_SECONDS = 3;
|
||||
}
|
||||
|
||||
message TerminalSize {
|
||||
uint32 rows = 1;
|
||||
uint32 columns = 2;
|
||||
}
|
||||
|
||||
message NetworkReservation {
|
||||
string uuid = 1;
|
||||
string ipv4 = 2;
|
||||
string ipv6 = 3;
|
||||
string mac = 4;
|
||||
string gateway_ipv4 = 5;
|
||||
string gateway_ipv6 = 6;
|
||||
string gateway_mac = 7;
|
||||
}
|
@ -1,275 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package krata.v1.control;
|
||||
|
||||
option java_multiple_files = true;
|
||||
option java_package = "dev.krata.proto.v1.control";
|
||||
option java_outer_classname = "ControlProto";
|
||||
|
||||
import "krata/idm/transport.proto";
|
||||
import "krata/v1/common.proto";
|
||||
|
||||
service ControlService {
|
||||
rpc GetHostStatus(GetHostStatusRequest) returns (GetHostStatusReply);
|
||||
rpc SnoopIdm(SnoopIdmRequest) returns (stream SnoopIdmReply);
|
||||
rpc GetHostCpuTopology(GetHostCpuTopologyRequest) returns (GetHostCpuTopologyReply);
|
||||
rpc SetHostPowerManagementPolicy(SetHostPowerManagementPolicyRequest) returns (SetHostPowerManagementPolicyReply);
|
||||
|
||||
rpc ListDevices(ListDevicesRequest) returns (ListDevicesReply);
|
||||
|
||||
rpc ListNetworkReservations(ListNetworkReservationsRequest) returns (ListNetworkReservationsReply);
|
||||
|
||||
rpc PullImage(PullImageRequest) returns (stream PullImageReply);
|
||||
|
||||
rpc CreateZone(CreateZoneRequest) returns (CreateZoneReply);
|
||||
rpc DestroyZone(DestroyZoneRequest) returns (DestroyZoneReply);
|
||||
|
||||
rpc ResolveZoneId(ResolveZoneIdRequest) returns (ResolveZoneIdReply);
|
||||
|
||||
rpc GetZone(GetZoneRequest) returns (GetZoneReply);
|
||||
|
||||
rpc UpdateZoneResources(UpdateZoneResourcesRequest) returns (UpdateZoneResourcesReply);
|
||||
|
||||
rpc ListZones(ListZonesRequest) returns (ListZonesReply);
|
||||
|
||||
rpc AttachZoneConsole(stream ZoneConsoleRequest) returns (stream ZoneConsoleReply);
|
||||
rpc ExecInsideZone(stream ExecInsideZoneRequest) returns (stream ExecInsideZoneReply);
|
||||
rpc ReadZoneMetrics(ReadZoneMetricsRequest) returns (ReadZoneMetricsReply);
|
||||
|
||||
rpc WatchEvents(WatchEventsRequest) returns (stream WatchEventsReply);
|
||||
|
||||
rpc ReadHypervisorConsole(ReadHypervisorConsoleRequest) returns (ReadHypervisorConsoleReply);
|
||||
}
|
||||
|
||||
message GetHostStatusRequest {}
|
||||
|
||||
message GetHostStatusReply {
|
||||
string host_uuid = 1;
|
||||
uint32 host_domid = 2;
|
||||
string krata_version = 3;
|
||||
string host_ipv4 = 4;
|
||||
string host_ipv6 = 5;
|
||||
string host_mac = 6;
|
||||
}
|
||||
|
||||
message CreateZoneRequest {
|
||||
krata.v1.common.ZoneSpec spec = 1;
|
||||
}
|
||||
|
||||
message CreateZoneReply {
|
||||
string zone_id = 1;
|
||||
}
|
||||
|
||||
message DestroyZoneRequest {
|
||||
string zone_id = 1;
|
||||
}
|
||||
|
||||
message DestroyZoneReply {}
|
||||
|
||||
message ResolveZoneIdRequest {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
message ResolveZoneIdReply {
|
||||
string zone_id = 1;
|
||||
}
|
||||
|
||||
message GetZoneRequest {
|
||||
string zone_id = 1;
|
||||
}
|
||||
|
||||
message GetZoneReply {
|
||||
krata.v1.common.Zone zone = 1;
|
||||
}
|
||||
|
||||
message ListZonesRequest {}
|
||||
|
||||
message ListZonesReply {
|
||||
repeated krata.v1.common.Zone zones = 1;
|
||||
}
|
||||
|
||||
message ExecInsideZoneRequest {
|
||||
string zone_id = 1;
|
||||
krata.v1.common.ZoneTaskSpec task = 2;
|
||||
bytes stdin = 3;
|
||||
bool stdin_closed = 4;
|
||||
krata.v1.common.TerminalSize terminal_size = 5;
|
||||
}
|
||||
|
||||
message ExecInsideZoneReply {
|
||||
bool exited = 1;
|
||||
string error = 2;
|
||||
int32 exit_code = 3;
|
||||
bytes stdout = 4;
|
||||
bytes stderr = 5;
|
||||
}
|
||||
|
||||
message ZoneConsoleRequest {
|
||||
string zone_id = 1;
|
||||
bytes data = 2;
|
||||
bool replay_history = 3;
|
||||
}
|
||||
|
||||
message ZoneConsoleReply {
|
||||
bytes data = 1;
|
||||
}
|
||||
|
||||
message WatchEventsRequest {}
|
||||
|
||||
message WatchEventsReply {
|
||||
oneof event {
|
||||
ZoneChangedEvent zone_changed = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message ZoneChangedEvent {
|
||||
krata.v1.common.Zone zone = 1;
|
||||
}
|
||||
|
||||
message ReadZoneMetricsRequest {
|
||||
string zone_id = 1;
|
||||
}
|
||||
|
||||
message ReadZoneMetricsReply {
|
||||
krata.v1.common.ZoneMetricNode root = 1;
|
||||
}
|
||||
|
||||
message SnoopIdmRequest {}
|
||||
|
||||
message SnoopIdmReply {
|
||||
string from = 1;
|
||||
string to = 2;
|
||||
krata.idm.transport.IdmTransportPacket packet = 3;
|
||||
}
|
||||
|
||||
message ImageProgress {
|
||||
ImageProgressPhase phase = 1;
|
||||
repeated ImageProgressLayer layers = 2;
|
||||
ImageProgressIndication indication = 3;
|
||||
}
|
||||
|
||||
enum ImageProgressPhase {
|
||||
IMAGE_PROGRESS_PHASE_UNKNOWN = 0;
|
||||
IMAGE_PROGRESS_PHASE_STARTED = 1;
|
||||
IMAGE_PROGRESS_PHASE_RESOLVING = 2;
|
||||
IMAGE_PROGRESS_PHASE_RESOLVED = 3;
|
||||
IMAGE_PROGRESS_PHASE_CONFIG_DOWNLOAD = 4;
|
||||
IMAGE_PROGRESS_PHASE_LAYER_DOWNLOAD = 5;
|
||||
IMAGE_PROGRESS_PHASE_ASSEMBLE = 6;
|
||||
IMAGE_PROGRESS_PHASE_PACK = 7;
|
||||
IMAGE_PROGRESS_PHASE_COMPLETE = 8;
|
||||
}
|
||||
|
||||
message ImageProgressLayer {
|
||||
string id = 1;
|
||||
ImageProgressLayerPhase phase = 2;
|
||||
ImageProgressIndication indication = 3;
|
||||
}
|
||||
|
||||
enum ImageProgressLayerPhase {
|
||||
IMAGE_PROGRESS_LAYER_PHASE_UNKNOWN = 0;
|
||||
IMAGE_PROGRESS_LAYER_PHASE_WAITING = 1;
|
||||
IMAGE_PROGRESS_LAYER_PHASE_DOWNLOADING = 2;
|
||||
IMAGE_PROGRESS_LAYER_PHASE_DOWNLOADED = 3;
|
||||
IMAGE_PROGRESS_LAYER_PHASE_EXTRACTING = 4;
|
||||
IMAGE_PROGRESS_LAYER_PHASE_EXTRACTED = 5;
|
||||
}
|
||||
|
||||
message ImageProgressIndication {
|
||||
oneof indication {
|
||||
ImageProgressIndicationBar bar = 1;
|
||||
ImageProgressIndicationSpinner spinner = 2;
|
||||
ImageProgressIndicationHidden hidden = 3;
|
||||
ImageProgressIndicationCompleted completed = 4;
|
||||
}
|
||||
}
|
||||
|
||||
message ImageProgressIndicationBar {
|
||||
string message = 1;
|
||||
uint64 current = 2;
|
||||
uint64 total = 3;
|
||||
bool is_bytes = 4;
|
||||
}
|
||||
|
||||
message ImageProgressIndicationSpinner {
|
||||
string message = 1;
|
||||
}
|
||||
|
||||
message ImageProgressIndicationHidden {}
|
||||
|
||||
message ImageProgressIndicationCompleted {
|
||||
string message = 1;
|
||||
uint64 total = 2;
|
||||
bool is_bytes = 3;
|
||||
}
|
||||
|
||||
message PullImageRequest {
|
||||
string image = 1;
|
||||
krata.v1.common.OciImageFormat format = 2;
|
||||
bool overwrite_cache = 3;
|
||||
bool update = 4;
|
||||
}
|
||||
|
||||
message PullImageReply {
|
||||
ImageProgress progress = 1;
|
||||
string digest = 2;
|
||||
krata.v1.common.OciImageFormat format = 3;
|
||||
}
|
||||
|
||||
message DeviceInfo {
|
||||
string name = 1;
|
||||
bool claimed = 2;
|
||||
string owner = 3;
|
||||
}
|
||||
|
||||
message ListDevicesRequest {}
|
||||
|
||||
message ListDevicesReply {
|
||||
repeated DeviceInfo devices = 1;
|
||||
}
|
||||
|
||||
enum HostCpuTopologyClass {
|
||||
HOST_CPU_TOPOLOGY_CLASS_STANDARD = 0;
|
||||
HOST_CPU_TOPOLOGY_CLASS_PERFORMANCE = 1;
|
||||
HOST_CPU_TOPOLOGY_CLASS_EFFICIENCY = 2;
|
||||
}
|
||||
|
||||
message HostCpuTopologyInfo {
|
||||
uint32 core = 1;
|
||||
uint32 socket = 2;
|
||||
uint32 node = 3;
|
||||
uint32 thread = 4;
|
||||
HostCpuTopologyClass class = 5;
|
||||
}
|
||||
|
||||
message GetHostCpuTopologyRequest {}
|
||||
|
||||
message GetHostCpuTopologyReply {
|
||||
repeated HostCpuTopologyInfo cpus = 1;
|
||||
}
|
||||
|
||||
message SetHostPowerManagementPolicyRequest {
|
||||
string scheduler = 1;
|
||||
bool smt_awareness = 2;
|
||||
}
|
||||
|
||||
message SetHostPowerManagementPolicyReply {}
|
||||
|
||||
message UpdateZoneResourcesRequest {
|
||||
string zone_id = 1;
|
||||
krata.v1.common.ZoneResourceSpec resources = 2;
|
||||
}
|
||||
|
||||
message UpdateZoneResourcesReply {}
|
||||
|
||||
message ReadHypervisorConsoleRequest {}
|
||||
|
||||
message ReadHypervisorConsoleReply {
|
||||
string data = 1;
|
||||
}
|
||||
|
||||
message ListNetworkReservationsRequest {}
|
||||
|
||||
message ListNetworkReservationsReply {
|
||||
repeated krata.v1.common.NetworkReservation reservations = 1;
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
#[cfg(unix)]
|
||||
use crate::unix::HyperUnixConnector;
|
||||
use crate::{dial::ControlDialAddress, v1::control::control_service_client::ControlServiceClient};
|
||||
#[cfg(not(unix))]
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Result;
|
||||
use tonic::transport::{Channel, ClientTlsConfig, Endpoint};
|
||||
|
||||
pub struct ControlClientProvider {}
|
||||
|
||||
impl ControlClientProvider {
|
||||
pub async fn dial(addr: ControlDialAddress) -> Result<ControlServiceClient<Channel>> {
|
||||
let channel = match addr {
|
||||
ControlDialAddress::UnixSocket { path } => {
|
||||
#[cfg(not(unix))]
|
||||
return Err(anyhow!(
|
||||
"unix sockets are not supported on this platform (path {})",
|
||||
path
|
||||
));
|
||||
#[cfg(unix)]
|
||||
ControlClientProvider::dial_unix_socket(path).await?
|
||||
}
|
||||
|
||||
ControlDialAddress::Tcp { host, port } => {
|
||||
Endpoint::try_from(format!("http://{}:{}", host, port))?
|
||||
.connect()
|
||||
.await?
|
||||
}
|
||||
|
||||
ControlDialAddress::Tls {
|
||||
host,
|
||||
port,
|
||||
insecure: _,
|
||||
} => {
|
||||
let tls_config = ClientTlsConfig::new().domain_name(&host);
|
||||
let address = format!("https://{}:{}", host, port);
|
||||
Channel::from_shared(address)?
|
||||
.tls_config(tls_config)?
|
||||
.connect()
|
||||
.await?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(ControlServiceClient::new(channel))
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn dial_unix_socket(path: String) -> Result<Channel> {
|
||||
// This URL is not actually used but is required to be specified.
|
||||
Ok(Endpoint::try_from(format!("unix://localhost/{}", path))?
|
||||
.connect_with_connector(HyperUnixConnector {})
|
||||
.await?)
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use url::{Host, Url};
|
||||
|
||||
pub const KRATA_DEFAULT_TCP_PORT: u16 = 4350;
|
||||
pub const KRATA_DEFAULT_TLS_PORT: u16 = 4353;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ControlDialAddress {
|
||||
UnixSocket {
|
||||
path: String,
|
||||
},
|
||||
Tcp {
|
||||
host: String,
|
||||
port: u16,
|
||||
},
|
||||
Tls {
|
||||
host: String,
|
||||
port: u16,
|
||||
insecure: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl FromStr for ControlDialAddress {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let url: Url = s.parse()?;
|
||||
|
||||
let host = url.host().unwrap_or(Host::Domain("localhost")).to_string();
|
||||
|
||||
match url.scheme() {
|
||||
"unix" => Ok(ControlDialAddress::UnixSocket {
|
||||
path: url.path().to_string(),
|
||||
}),
|
||||
|
||||
"tcp" => {
|
||||
let port = url.port().unwrap_or(KRATA_DEFAULT_TCP_PORT);
|
||||
Ok(ControlDialAddress::Tcp { host, port })
|
||||
}
|
||||
|
||||
"tls" | "tls-insecure" => {
|
||||
let insecure = url.scheme() == "tls-insecure";
|
||||
let port = url.port().unwrap_or(KRATA_DEFAULT_TLS_PORT);
|
||||
Ok(ControlDialAddress::Tls {
|
||||
host,
|
||||
port,
|
||||
insecure,
|
||||
})
|
||||
}
|
||||
|
||||
_ => Err(anyhow!("unknown control address scheme: {}", url.scheme())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ControlDialAddress> for Url {
|
||||
fn from(val: ControlDialAddress) -> Self {
|
||||
match val {
|
||||
ControlDialAddress::UnixSocket { path } => {
|
||||
let mut url = Url::parse("unix:///").unwrap();
|
||||
url.set_path(&path);
|
||||
url
|
||||
}
|
||||
|
||||
ControlDialAddress::Tcp { host, port } => {
|
||||
let mut url = Url::parse("tcp://").unwrap();
|
||||
url.set_host(Some(&host)).unwrap();
|
||||
if port != KRATA_DEFAULT_TCP_PORT {
|
||||
url.set_port(Some(port)).unwrap();
|
||||
}
|
||||
url
|
||||
}
|
||||
|
||||
ControlDialAddress::Tls {
|
||||
host,
|
||||
port,
|
||||
insecure,
|
||||
} => {
|
||||
let mut url = Url::parse("tls://").unwrap();
|
||||
if insecure {
|
||||
url.set_scheme("tls-insecure").unwrap();
|
||||
}
|
||||
url.set_host(Some(&host)).unwrap();
|
||||
if port != KRATA_DEFAULT_TLS_PORT {
|
||||
url.set_port(Some(port)).unwrap();
|
||||
}
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ControlDialAddress {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let url: Url = self.clone().into();
|
||||
write!(f, "{}", url)
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
use std::{
|
||||
os::fd::{AsRawFd, FromRawFd, OwnedFd},
|
||||
ptr::addr_of_mut,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use libc::{ioctl, socket, AF_INET, SOCK_DGRAM};
|
||||
|
||||
#[repr(C)]
|
||||
struct EthtoolValue {
|
||||
cmd: u32,
|
||||
data: u32,
|
||||
}
|
||||
|
||||
const ETHTOOL_SGSO: u32 = 0x00000024;
|
||||
const ETHTOOL_STSO: u32 = 0x0000001f;
|
||||
|
||||
#[cfg(not(target_env = "musl"))]
|
||||
const SIOCETHTOOL: libc::c_ulong = libc::SIOCETHTOOL;
|
||||
#[cfg(target_env = "musl")]
|
||||
const SIOCETHTOOL: libc::c_int = libc::SIOCETHTOOL as i32;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug)]
|
||||
struct EthtoolIfreq {
|
||||
ifr_name: [libc::c_char; libc::IF_NAMESIZE],
|
||||
ifr_data: libc::uintptr_t,
|
||||
}
|
||||
|
||||
impl EthtoolIfreq {
|
||||
fn new(interface: &str) -> EthtoolIfreq {
|
||||
let mut ifreq = EthtoolIfreq {
|
||||
ifr_name: [0; libc::IF_NAMESIZE],
|
||||
ifr_data: 0,
|
||||
};
|
||||
for (i, byte) in interface.as_bytes().iter().enumerate() {
|
||||
ifreq.ifr_name[i] = *byte as libc::c_char
|
||||
}
|
||||
ifreq
|
||||
}
|
||||
|
||||
fn set_value(&mut self, ptr: *mut libc::c_void) {
|
||||
self.ifr_data = ptr as libc::uintptr_t;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EthtoolHandle {
|
||||
fd: OwnedFd,
|
||||
}
|
||||
|
||||
impl EthtoolHandle {
|
||||
pub fn new() -> Result<EthtoolHandle> {
|
||||
let fd = unsafe { socket(AF_INET, SOCK_DGRAM, 0) };
|
||||
if fd == -1 {
|
||||
return Err(std::io::Error::last_os_error().into());
|
||||
}
|
||||
|
||||
Ok(EthtoolHandle {
|
||||
fd: unsafe { OwnedFd::from_raw_fd(fd) },
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_gso(&mut self, interface: &str, value: bool) -> Result<()> {
|
||||
self.set_value(interface, ETHTOOL_SGSO, if value { 1 } else { 0 })
|
||||
}
|
||||
|
||||
pub fn set_tso(&mut self, interface: &str, value: bool) -> Result<()> {
|
||||
self.set_value(interface, ETHTOOL_STSO, if value { 1 } else { 0 })
|
||||
}
|
||||
|
||||
fn set_value(&mut self, interface: &str, cmd: u32, value: u32) -> Result<()> {
|
||||
let mut ifreq = EthtoolIfreq::new(interface);
|
||||
let mut value = EthtoolValue { cmd, data: value };
|
||||
ifreq.set_value(addr_of_mut!(value) as *mut libc::c_void);
|
||||
let result = unsafe { ioctl(self.fd.as_raw_fd(), SIOCETHTOOL, addr_of_mut!(ifreq) as u64) };
|
||||
if result == -1 {
|
||||
return Err(std::io::Error::last_os_error().into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use crate::v1::control::{
|
||||
control_service_client::ControlServiceClient, watch_events_reply::Event, WatchEventsReply,
|
||||
WatchEventsRequest,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use log::{error, trace, warn};
|
||||
use tokio::{sync::broadcast, task::JoinHandle, time::sleep};
|
||||
use tokio_stream::StreamExt;
|
||||
use tonic::{transport::Channel, Streaming};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EventStream {
|
||||
sender: Arc<broadcast::Sender<Event>>,
|
||||
task: Arc<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl EventStream {
|
||||
pub async fn open(client: ControlServiceClient<Channel>) -> Result<Self> {
|
||||
let (sender, _) = broadcast::channel(1000);
|
||||
let emit = sender.clone();
|
||||
let task = tokio::task::spawn(async move {
|
||||
if let Err(error) = EventStream::process(client, emit).await {
|
||||
error!("failed to process event stream: {}", error);
|
||||
}
|
||||
});
|
||||
Ok(Self {
|
||||
sender: Arc::new(sender),
|
||||
task: Arc::new(task),
|
||||
})
|
||||
}
|
||||
|
||||
async fn process(
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
emit: broadcast::Sender<Event>,
|
||||
) -> Result<()> {
|
||||
let mut events: Option<Streaming<WatchEventsReply>> = None;
|
||||
loop {
|
||||
let mut stream = match events {
|
||||
Some(stream) => stream,
|
||||
None => {
|
||||
let result = client.watch_events(WatchEventsRequest {}).await;
|
||||
if let Err(error) = result {
|
||||
warn!("failed to watch events: {}", error);
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
continue;
|
||||
}
|
||||
result.unwrap().into_inner()
|
||||
}
|
||||
};
|
||||
|
||||
let Some(result) = stream.next().await else {
|
||||
events = None;
|
||||
continue;
|
||||
};
|
||||
|
||||
let reply = match result {
|
||||
Ok(reply) => reply,
|
||||
Err(error) => {
|
||||
trace!("event stream processing failed: {}", error);
|
||||
events = None;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(event) = reply.event else {
|
||||
events = Some(stream);
|
||||
continue;
|
||||
};
|
||||
let _ = emit.send(event);
|
||||
events = Some(stream);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<Event> {
|
||||
self.sender.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EventStream {
|
||||
fn drop(&mut self) {
|
||||
if Arc::strong_count(&self.task) <= 1 {
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,542 +0,0 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::Path,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use bytes::{Buf, BufMut, BytesMut};
|
||||
use log::{debug, error};
|
||||
use nix::sys::termios::{cfmakeraw, tcgetattr, tcsetattr, SetArg};
|
||||
use prost::Message;
|
||||
use tokio::{
|
||||
fs::File,
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
select,
|
||||
sync::{
|
||||
broadcast,
|
||||
mpsc::{self, Receiver, Sender},
|
||||
oneshot, Mutex,
|
||||
},
|
||||
task::JoinHandle,
|
||||
time::timeout,
|
||||
};
|
||||
|
||||
use super::{
|
||||
internal,
|
||||
serialize::{IdmRequest, IdmSerializable},
|
||||
transport::{IdmTransportPacket, IdmTransportPacketForm},
|
||||
};
|
||||
|
||||
type OneshotRequestMap<R> = Arc<Mutex<HashMap<u64, oneshot::Sender<<R as IdmRequest>::Response>>>>;
|
||||
type StreamRequestMap<R> = Arc<Mutex<HashMap<u64, Sender<<R as IdmRequest>::Response>>>>;
|
||||
type StreamRequestUpdateMap<R> = Arc<Mutex<HashMap<u64, Sender<R>>>>;
|
||||
pub type IdmInternalClient = IdmClient<internal::Request, internal::Event>;
|
||||
|
||||
const IDM_PACKET_QUEUE_LEN: usize = 100;
|
||||
const IDM_REQUEST_TIMEOUT_SECS: u64 = 10;
|
||||
const IDM_PACKET_MAX_SIZE: usize = 20 * 1024 * 1024;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait IdmBackend: Send {
|
||||
async fn recv(&mut self) -> Result<Vec<IdmTransportPacket>>;
|
||||
async fn send(&mut self, packet: IdmTransportPacket) -> Result<()>;
|
||||
}
|
||||
|
||||
pub struct IdmFileBackend {
|
||||
read: Arc<Mutex<File>>,
|
||||
read_buffer: BytesMut,
|
||||
write: Arc<Mutex<File>>,
|
||||
}
|
||||
|
||||
impl IdmFileBackend {
|
||||
pub async fn new(read_file: File, write_file: File) -> Result<IdmFileBackend> {
|
||||
IdmFileBackend::set_raw_port(&read_file)?;
|
||||
IdmFileBackend::set_raw_port(&write_file)?;
|
||||
Ok(IdmFileBackend {
|
||||
read: Arc::new(Mutex::new(read_file)),
|
||||
read_buffer: BytesMut::new(),
|
||||
write: Arc::new(Mutex::new(write_file)),
|
||||
})
|
||||
}
|
||||
|
||||
fn set_raw_port(file: &File) -> Result<()> {
|
||||
let mut termios = tcgetattr(file)?;
|
||||
cfmakeraw(&mut termios);
|
||||
tcsetattr(file, SetArg::TCSANOW, &termios)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl IdmBackend for IdmFileBackend {
|
||||
async fn recv(&mut self) -> Result<Vec<IdmTransportPacket>> {
|
||||
let mut data = vec![0; 8192];
|
||||
let mut first = true;
|
||||
'read_more: loop {
|
||||
let mut packets = Vec::new();
|
||||
if !first {
|
||||
if !packets.is_empty() {
|
||||
return Ok(packets);
|
||||
}
|
||||
let size = self.read.lock().await.read(&mut data).await?;
|
||||
self.read_buffer.extend_from_slice(&data[0..size]);
|
||||
}
|
||||
first = false;
|
||||
loop {
|
||||
if self.read_buffer.len() < 6 {
|
||||
continue 'read_more;
|
||||
}
|
||||
|
||||
let b1 = self.read_buffer[0];
|
||||
let b2 = self.read_buffer[1];
|
||||
|
||||
if b1 != 0xff || b2 != 0xff {
|
||||
self.read_buffer.clear();
|
||||
continue 'read_more;
|
||||
}
|
||||
|
||||
let size = (self.read_buffer[2] as u32
|
||||
| (self.read_buffer[3] as u32) << 8
|
||||
| (self.read_buffer[4] as u32) << 16
|
||||
| (self.read_buffer[5] as u32) << 24) as usize;
|
||||
let needed = size + 6;
|
||||
if self.read_buffer.len() < needed {
|
||||
continue 'read_more;
|
||||
}
|
||||
|
||||
let mut packet = self.read_buffer.split_to(needed);
|
||||
packet.advance(6);
|
||||
|
||||
match IdmTransportPacket::decode(packet) {
|
||||
Ok(packet) => {
|
||||
packets.push(packet);
|
||||
}
|
||||
Err(error) => {
|
||||
return Err(anyhow!("received invalid idm packet: {}", error));
|
||||
}
|
||||
}
|
||||
|
||||
if self.read_buffer.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Ok(packets);
|
||||
}
|
||||
}
|
||||
|
||||
async fn send(&mut self, packet: IdmTransportPacket) -> Result<()> {
|
||||
let mut file = self.write.lock().await;
|
||||
let length = packet.encoded_len();
|
||||
let mut buffer = BytesMut::with_capacity(6 + length);
|
||||
buffer.put_slice(&[0xff, 0xff]);
|
||||
buffer.put_u32_le(length as u32);
|
||||
packet.encode(&mut buffer)?;
|
||||
file.write_all(&buffer).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IdmClient<R: IdmRequest, E: IdmSerializable> {
|
||||
channel: u64,
|
||||
request_backend_sender: broadcast::Sender<(u64, R)>,
|
||||
request_stream_backend_sender: broadcast::Sender<IdmClientStreamResponseHandle<R>>,
|
||||
next_request_id: Arc<Mutex<u64>>,
|
||||
event_receiver_sender: broadcast::Sender<E>,
|
||||
tx_sender: Sender<IdmTransportPacket>,
|
||||
requests: OneshotRequestMap<R>,
|
||||
request_streams: StreamRequestMap<R>,
|
||||
task: Arc<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl<R: IdmRequest, E: IdmSerializable> Drop for IdmClient<R, E> {
|
||||
fn drop(&mut self) {
|
||||
if Arc::strong_count(&self.task) <= 1 {
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IdmClientStreamRequestHandle<R: IdmRequest, E: IdmSerializable> {
|
||||
pub id: u64,
|
||||
pub receiver: Receiver<R::Response>,
|
||||
pub client: IdmClient<R, E>,
|
||||
}
|
||||
|
||||
impl<R: IdmRequest, E: IdmSerializable> IdmClientStreamRequestHandle<R, E> {
|
||||
pub async fn update(&self, request: R) -> Result<()> {
|
||||
self.client
|
||||
.tx_sender
|
||||
.send(IdmTransportPacket {
|
||||
id: self.id,
|
||||
channel: self.client.channel,
|
||||
form: IdmTransportPacketForm::StreamRequestUpdate.into(),
|
||||
data: request.encode()?,
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: IdmRequest, E: IdmSerializable> Drop for IdmClientStreamRequestHandle<R, E> {
|
||||
fn drop(&mut self) {
|
||||
let id = self.id;
|
||||
let client = self.client.clone();
|
||||
tokio::task::spawn(async move {
|
||||
let _ = client
|
||||
.tx_sender
|
||||
.send(IdmTransportPacket {
|
||||
id,
|
||||
channel: client.channel,
|
||||
form: IdmTransportPacketForm::StreamRequestClosed.into(),
|
||||
data: vec![],
|
||||
})
|
||||
.await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IdmClientStreamResponseHandle<R: IdmRequest> {
|
||||
pub initial: R,
|
||||
pub id: u64,
|
||||
channel: u64,
|
||||
tx_sender: Sender<IdmTransportPacket>,
|
||||
receiver: Arc<Mutex<Option<Receiver<R>>>>,
|
||||
}
|
||||
|
||||
impl<R: IdmRequest> IdmClientStreamResponseHandle<R> {
|
||||
pub async fn respond(&self, response: R::Response) -> Result<()> {
|
||||
self.tx_sender
|
||||
.send(IdmTransportPacket {
|
||||
id: self.id,
|
||||
channel: self.channel,
|
||||
form: IdmTransportPacketForm::StreamResponseUpdate.into(),
|
||||
data: response.encode()?,
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn take(&self) -> Result<Receiver<R>> {
|
||||
let mut guard = self.receiver.lock().await;
|
||||
let Some(receiver) = (*guard).take() else {
|
||||
return Err(anyhow!("request has already been claimed!"));
|
||||
};
|
||||
Ok(receiver)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: IdmRequest> Drop for IdmClientStreamResponseHandle<R> {
|
||||
fn drop(&mut self) {
|
||||
if Arc::strong_count(&self.receiver) <= 1 {
|
||||
let id = self.id;
|
||||
let channel = self.channel;
|
||||
let tx_sender = self.tx_sender.clone();
|
||||
tokio::task::spawn(async move {
|
||||
let _ = tx_sender
|
||||
.send(IdmTransportPacket {
|
||||
id,
|
||||
channel,
|
||||
form: IdmTransportPacketForm::StreamResponseClosed.into(),
|
||||
data: vec![],
|
||||
})
|
||||
.await;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: IdmRequest, E: IdmSerializable> IdmClient<R, E> {
|
||||
pub async fn new(channel: u64, backend: Box<dyn IdmBackend>) -> Result<Self> {
|
||||
let requests = Arc::new(Mutex::new(HashMap::new()));
|
||||
let request_streams = Arc::new(Mutex::new(HashMap::new()));
|
||||
let request_update_streams = Arc::new(Mutex::new(HashMap::new()));
|
||||
let (event_sender, event_receiver) = broadcast::channel(IDM_PACKET_QUEUE_LEN);
|
||||
let (internal_request_backend_sender, _) = broadcast::channel(IDM_PACKET_QUEUE_LEN);
|
||||
let (internal_request_stream_backend_sender, _) = broadcast::channel(IDM_PACKET_QUEUE_LEN);
|
||||
let (tx_sender, tx_receiver) = mpsc::channel(IDM_PACKET_QUEUE_LEN);
|
||||
let backend_event_sender = event_sender.clone();
|
||||
let request_backend_sender = internal_request_backend_sender.clone();
|
||||
let request_stream_backend_sender = internal_request_stream_backend_sender.clone();
|
||||
let requests_for_client = requests.clone();
|
||||
let request_streams_for_client = request_streams.clone();
|
||||
let tx_sender_for_client = tx_sender.clone();
|
||||
let task = tokio::task::spawn(async move {
|
||||
if let Err(error) = IdmClient::process(
|
||||
backend,
|
||||
channel,
|
||||
tx_sender,
|
||||
backend_event_sender,
|
||||
requests,
|
||||
request_streams,
|
||||
request_update_streams,
|
||||
internal_request_backend_sender,
|
||||
internal_request_stream_backend_sender,
|
||||
event_receiver,
|
||||
tx_receiver,
|
||||
)
|
||||
.await
|
||||
{
|
||||
debug!("failed to handle idm client processing: {}", error);
|
||||
}
|
||||
});
|
||||
Ok(IdmClient {
|
||||
channel,
|
||||
next_request_id: Arc::new(Mutex::new(0)),
|
||||
event_receiver_sender: event_sender.clone(),
|
||||
request_backend_sender,
|
||||
request_stream_backend_sender,
|
||||
requests: requests_for_client,
|
||||
request_streams: request_streams_for_client,
|
||||
tx_sender: tx_sender_for_client,
|
||||
task: Arc::new(task),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn open<P: AsRef<Path>>(channel: u64, path: P) -> Result<Self> {
|
||||
let read_file = File::options()
|
||||
.read(true)
|
||||
.write(false)
|
||||
.create(false)
|
||||
.open(&path)
|
||||
.await?;
|
||||
let write_file = File::options()
|
||||
.read(false)
|
||||
.write(true)
|
||||
.create(false)
|
||||
.open(path)
|
||||
.await?;
|
||||
let backend = IdmFileBackend::new(read_file, write_file).await?;
|
||||
IdmClient::new(channel, Box::new(backend) as Box<dyn IdmBackend>).await
|
||||
}
|
||||
|
||||
pub async fn emit<T: IdmSerializable>(&self, event: T) -> Result<()> {
|
||||
let id = {
|
||||
let mut guard = self.next_request_id.lock().await;
|
||||
let req = *guard;
|
||||
*guard = req.wrapping_add(1);
|
||||
req
|
||||
};
|
||||
self.tx_sender
|
||||
.send(IdmTransportPacket {
|
||||
id,
|
||||
form: IdmTransportPacketForm::Event.into(),
|
||||
channel: self.channel,
|
||||
data: event.encode()?,
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn requests(&self) -> Result<broadcast::Receiver<(u64, R)>> {
|
||||
Ok(self.request_backend_sender.subscribe())
|
||||
}
|
||||
|
||||
pub async fn request_streams(
|
||||
&self,
|
||||
) -> Result<broadcast::Receiver<IdmClientStreamResponseHandle<R>>> {
|
||||
Ok(self.request_stream_backend_sender.subscribe())
|
||||
}
|
||||
|
||||
pub async fn respond<T: IdmSerializable>(&self, id: u64, response: T) -> Result<()> {
|
||||
let packet = IdmTransportPacket {
|
||||
id,
|
||||
form: IdmTransportPacketForm::Response.into(),
|
||||
channel: self.channel,
|
||||
data: response.encode()?,
|
||||
};
|
||||
self.tx_sender.send(packet).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn subscribe(&self) -> Result<broadcast::Receiver<E>> {
|
||||
Ok(self.event_receiver_sender.subscribe())
|
||||
}
|
||||
|
||||
pub async fn send(&self, request: R) -> Result<R::Response> {
|
||||
let (sender, receiver) = oneshot::channel::<R::Response>();
|
||||
let req = {
|
||||
let mut guard = self.next_request_id.lock().await;
|
||||
let req = *guard;
|
||||
*guard = req.wrapping_add(1);
|
||||
req
|
||||
};
|
||||
let mut requests = self.requests.lock().await;
|
||||
requests.insert(req, sender);
|
||||
drop(requests);
|
||||
let success = AtomicBool::new(false);
|
||||
let _guard = scopeguard::guard(self.requests.clone(), |requests| {
|
||||
if success.load(Ordering::Acquire) {
|
||||
return;
|
||||
}
|
||||
tokio::task::spawn(async move {
|
||||
let mut requests = requests.lock().await;
|
||||
requests.remove(&req);
|
||||
});
|
||||
});
|
||||
self.tx_sender
|
||||
.send(IdmTransportPacket {
|
||||
id: req,
|
||||
channel: self.channel,
|
||||
form: IdmTransportPacketForm::Request.into(),
|
||||
data: request.encode()?,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let response = timeout(Duration::from_secs(IDM_REQUEST_TIMEOUT_SECS), receiver).await??;
|
||||
success.store(true, Ordering::Release);
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn send_stream(&self, request: R) -> Result<IdmClientStreamRequestHandle<R, E>> {
|
||||
let (sender, receiver) = mpsc::channel::<R::Response>(100);
|
||||
let req = {
|
||||
let mut guard = self.next_request_id.lock().await;
|
||||
let req = *guard;
|
||||
*guard = req.wrapping_add(1);
|
||||
req
|
||||
};
|
||||
let mut requests = self.request_streams.lock().await;
|
||||
requests.insert(req, sender);
|
||||
drop(requests);
|
||||
self.tx_sender
|
||||
.send(IdmTransportPacket {
|
||||
id: req,
|
||||
channel: self.channel,
|
||||
form: IdmTransportPacketForm::StreamRequest.into(),
|
||||
data: request.encode()?,
|
||||
})
|
||||
.await?;
|
||||
Ok(IdmClientStreamRequestHandle {
|
||||
id: req,
|
||||
receiver,
|
||||
client: self.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn process(
|
||||
mut backend: Box<dyn IdmBackend>,
|
||||
channel: u64,
|
||||
tx_sender: Sender<IdmTransportPacket>,
|
||||
event_sender: broadcast::Sender<E>,
|
||||
requests: OneshotRequestMap<R>,
|
||||
request_streams: StreamRequestMap<R>,
|
||||
request_update_streams: StreamRequestUpdateMap<R>,
|
||||
request_backend_sender: broadcast::Sender<(u64, R)>,
|
||||
request_stream_backend_sender: broadcast::Sender<IdmClientStreamResponseHandle<R>>,
|
||||
_event_receiver: broadcast::Receiver<E>,
|
||||
mut receiver: Receiver<IdmTransportPacket>,
|
||||
) -> Result<()> {
|
||||
loop {
|
||||
select! {
|
||||
x = backend.recv() => match x {
|
||||
Ok(packets) => {
|
||||
for packet in packets {
|
||||
if packet.channel != channel {
|
||||
continue;
|
||||
}
|
||||
|
||||
match packet.form() {
|
||||
IdmTransportPacketForm::Event => {
|
||||
if let Ok(event) = E::decode(&packet.data) {
|
||||
let _ = event_sender.send(event);
|
||||
}
|
||||
},
|
||||
|
||||
IdmTransportPacketForm::Request => {
|
||||
if let Ok(request) = R::decode(&packet.data) {
|
||||
let _ = request_backend_sender.send((packet.id, request));
|
||||
}
|
||||
},
|
||||
|
||||
IdmTransportPacketForm::Response => {
|
||||
let mut requests = requests.lock().await;
|
||||
if let Some(sender) = requests.remove(&packet.id) {
|
||||
drop(requests);
|
||||
|
||||
if let Ok(response) = R::Response::decode(&packet.data) {
|
||||
let _ = sender.send(response);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
IdmTransportPacketForm::StreamRequest => {
|
||||
if let Ok(request) = R::decode(&packet.data) {
|
||||
let mut update_streams = request_update_streams.lock().await;
|
||||
let (sender, receiver) = mpsc::channel(100);
|
||||
update_streams.insert(packet.id, sender.clone());
|
||||
let handle = IdmClientStreamResponseHandle {
|
||||
initial: request,
|
||||
id: packet.id,
|
||||
channel,
|
||||
tx_sender: tx_sender.clone(),
|
||||
receiver: Arc::new(Mutex::new(Some(receiver))),
|
||||
};
|
||||
let _ = request_stream_backend_sender.send(handle);
|
||||
}
|
||||
}
|
||||
|
||||
IdmTransportPacketForm::StreamRequestUpdate => {
|
||||
if let Ok(request) = R::decode(&packet.data) {
|
||||
let mut update_streams = request_update_streams.lock().await;
|
||||
if let Some(stream) = update_streams.get_mut(&packet.id) {
|
||||
let _ = stream.try_send(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IdmTransportPacketForm::StreamRequestClosed => {
|
||||
let mut update_streams = request_update_streams.lock().await;
|
||||
update_streams.remove(&packet.id);
|
||||
println!("stream request closed: {}", packet.id);
|
||||
}
|
||||
|
||||
IdmTransportPacketForm::StreamResponseUpdate => {
|
||||
let requests = request_streams.lock().await;
|
||||
if let Some(sender) = requests.get(&packet.id) {
|
||||
if let Ok(response) = R::Response::decode(&packet.data) {
|
||||
let _ = sender.try_send(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IdmTransportPacketForm::StreamResponseClosed => {
|
||||
let mut requests = request_streams.lock().await;
|
||||
requests.remove(&packet.id);
|
||||
}
|
||||
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Err(error) => {
|
||||
return Err(anyhow!("failed to read idm client: {}", error));
|
||||
}
|
||||
},
|
||||
x = receiver.recv() => match x {
|
||||
Some(packet) => {
|
||||
let length = packet.encoded_len();
|
||||
if length > IDM_PACKET_MAX_SIZE {
|
||||
error!("unable to send idm packet, packet size exceeded (tried to send {} bytes)", length);
|
||||
continue;
|
||||
}
|
||||
backend.send(packet.clone()).await?;
|
||||
},
|
||||
|
||||
None => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,129 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use prost::Message;
|
||||
use prost_types::{ListValue, Value};
|
||||
|
||||
use super::serialize::{IdmRequest, IdmSerializable};
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/krata.idm.internal.rs"));
|
||||
|
||||
pub const INTERNAL_IDM_CHANNEL: u64 = 0;
|
||||
|
||||
impl IdmSerializable for Event {
|
||||
fn encode(&self) -> Result<Vec<u8>> {
|
||||
Ok(self.encode_to_vec())
|
||||
}
|
||||
|
||||
fn decode(bytes: &[u8]) -> Result<Self> {
|
||||
Ok(<Self as prost::Message>::decode(bytes)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl IdmSerializable for Request {
|
||||
fn encode(&self) -> Result<Vec<u8>> {
|
||||
Ok(self.encode_to_vec())
|
||||
}
|
||||
|
||||
fn decode(bytes: &[u8]) -> Result<Self> {
|
||||
Ok(<Self as prost::Message>::decode(bytes)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl IdmRequest for Request {
|
||||
type Response = Response;
|
||||
}
|
||||
|
||||
impl IdmSerializable for Response {
|
||||
fn encode(&self) -> Result<Vec<u8>> {
|
||||
Ok(self.encode_to_vec())
|
||||
}
|
||||
|
||||
fn decode(bytes: &[u8]) -> Result<Self> {
|
||||
Ok(<Self as prost::Message>::decode(bytes)?)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AsIdmMetricValue {
|
||||
fn as_metric_value(&self) -> Value;
|
||||
}
|
||||
|
||||
impl MetricNode {
|
||||
pub fn structural<N: AsRef<str>>(name: N, children: Vec<MetricNode>) -> MetricNode {
|
||||
MetricNode {
|
||||
name: name.as_ref().to_string(),
|
||||
value: None,
|
||||
format: MetricFormat::Unknown.into(),
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn raw_value<N: AsRef<str>, V: AsIdmMetricValue>(name: N, value: V) -> MetricNode {
|
||||
MetricNode {
|
||||
name: name.as_ref().to_string(),
|
||||
value: Some(value.as_metric_value()),
|
||||
format: MetricFormat::Unknown.into(),
|
||||
children: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn value<N: AsRef<str>, V: AsIdmMetricValue>(
|
||||
name: N,
|
||||
value: V,
|
||||
format: MetricFormat,
|
||||
) -> MetricNode {
|
||||
MetricNode {
|
||||
name: name.as_ref().to_string(),
|
||||
value: Some(value.as_metric_value()),
|
||||
format: format.into(),
|
||||
children: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsIdmMetricValue for String {
|
||||
fn as_metric_value(&self) -> Value {
|
||||
Value {
|
||||
kind: Some(prost_types::value::Kind::StringValue(self.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsIdmMetricValue for &str {
|
||||
fn as_metric_value(&self) -> Value {
|
||||
Value {
|
||||
kind: Some(prost_types::value::Kind::StringValue(self.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsIdmMetricValue for u64 {
|
||||
fn as_metric_value(&self) -> Value {
|
||||
numeric(*self as f64)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsIdmMetricValue for i64 {
|
||||
fn as_metric_value(&self) -> Value {
|
||||
numeric(*self as f64)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsIdmMetricValue for f64 {
|
||||
fn as_metric_value(&self) -> Value {
|
||||
numeric(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsIdmMetricValue> AsIdmMetricValue for Vec<T> {
|
||||
fn as_metric_value(&self) -> Value {
|
||||
let values = self.iter().map(|x| x.as_metric_value()).collect::<_>();
|
||||
Value {
|
||||
kind: Some(prost_types::value::Kind::ListValue(ListValue { values })),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn numeric(value: f64) -> Value {
|
||||
Value {
|
||||
kind: Some(prost_types::value::Kind::NumberValue(value)),
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
#[cfg(unix)]
|
||||
pub mod client;
|
||||
pub mod internal;
|
||||
pub mod serialize;
|
||||
pub mod transport;
|
@ -1,10 +0,0 @@
|
||||
use anyhow::Result;
|
||||
|
||||
pub trait IdmSerializable: Sized + Clone + Send + Sync + 'static {
|
||||
fn decode(bytes: &[u8]) -> Result<Self>;
|
||||
fn encode(&self) -> Result<Vec<u8>>;
|
||||
}
|
||||
|
||||
pub trait IdmRequest: IdmSerializable {
|
||||
type Response: IdmSerializable;
|
||||
}
|
@ -1 +0,0 @@
|
||||
include!(concat!(env!("OUT_DIR"), "/krata.idm.transport.rs"));
|
@ -1,48 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum LaunchPackedFormat {
|
||||
Squashfs,
|
||||
Erofs,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct LaunchNetworkIpv4 {
|
||||
pub address: String,
|
||||
pub gateway: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct LaunchNetworkIpv6 {
|
||||
pub address: String,
|
||||
pub gateway: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct LaunchNetworkResolver {
|
||||
pub nameservers: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct LaunchNetwork {
|
||||
pub link: String,
|
||||
pub ipv4: LaunchNetworkIpv4,
|
||||
pub ipv6: LaunchNetworkIpv6,
|
||||
pub resolver: LaunchNetworkResolver,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct LaunchRoot {
|
||||
pub format: LaunchPackedFormat,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct LaunchInfo {
|
||||
pub root: LaunchRoot,
|
||||
pub hostname: Option<String>,
|
||||
pub network: Option<LaunchNetwork>,
|
||||
pub env: HashMap<String, String>,
|
||||
pub run: Option<Vec<String>>,
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use prost_reflect::DescriptorPool;
|
||||
|
||||
pub mod v1;
|
||||
|
||||
pub mod client;
|
||||
pub mod dial;
|
||||
pub mod events;
|
||||
pub mod idm;
|
||||
pub mod launchcfg;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod ethtool;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub mod unix;
|
||||
|
||||
pub static DESCRIPTOR_POOL: Lazy<DescriptorPool> = Lazy::new(|| {
|
||||
DescriptorPool::decode(
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/file_descriptor_set.bin")).as_ref(),
|
||||
)
|
||||
.unwrap()
|
||||
});
|
@ -1,73 +0,0 @@
|
||||
use std::future::Future;
|
||||
use std::io::Error;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use hyper::rt::ReadBufCursor;
|
||||
use hyper_util::rt::TokioIo;
|
||||
use pin_project_lite::pin_project;
|
||||
use tokio::io::AsyncWrite;
|
||||
use tokio::net::UnixStream;
|
||||
use tonic::transport::Uri;
|
||||
use tower::Service;
|
||||
|
||||
pin_project! {
|
||||
#[derive(Debug)]
|
||||
pub struct HyperUnixStream {
|
||||
#[pin]
|
||||
pub stream: UnixStream,
|
||||
}
|
||||
}
|
||||
|
||||
impl hyper::rt::Read for HyperUnixStream {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: ReadBufCursor<'_>,
|
||||
) -> Poll<Result<(), Error>> {
|
||||
let mut tokio = TokioIo::new(self.project().stream);
|
||||
Pin::new(&mut tokio).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl hyper::rt::Write for HyperUnixStream {
|
||||
fn poll_write(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize, Error>> {
|
||||
self.project().stream.poll_write(cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
|
||||
self.project().stream.poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
|
||||
self.project().stream.poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HyperUnixConnector;
|
||||
|
||||
impl Service<Uri> for HyperUnixConnector {
|
||||
type Response = HyperUnixStream;
|
||||
type Error = Error;
|
||||
#[allow(clippy::type_complexity)]
|
||||
type Future =
|
||||
Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
|
||||
|
||||
fn call(&mut self, req: Uri) -> Self::Future {
|
||||
let fut = async move {
|
||||
let path = req.path().to_string();
|
||||
let stream = UnixStream::connect(path).await?;
|
||||
Ok(HyperUnixStream { stream })
|
||||
};
|
||||
|
||||
Box::pin(fut)
|
||||
}
|
||||
|
||||
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
#![allow(clippy::all)]
|
||||
tonic::include_proto!("krata.v1.common");
|
@ -1,2 +0,0 @@
|
||||
#![allow(clippy::all)]
|
||||
tonic::include_proto!("krata.v1.control");
|
@ -1,2 +0,0 @@
|
||||
pub mod common;
|
||||
pub mod control;
|
@ -1,40 +0,0 @@
|
||||
[package]
|
||||
name = "krata-network"
|
||||
description = "Networking services for the krata isolation engine"
|
||||
license.workspace = true
|
||||
version.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
etherparse = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
krata = { path = "../krata", version = "^0.0.21" }
|
||||
krata-advmac = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
log = { workspace = true }
|
||||
rtnetlink = { workspace = true }
|
||||
smoltcp = { workspace = true }
|
||||
tonic = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-tun = { workspace = true }
|
||||
udp-stream = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
||||
[lib]
|
||||
name = "kratanet"
|
||||
|
||||
[[bin]]
|
||||
name = "kratanet"
|
||||
path = "bin/network.rs"
|
||||
|
||||
[[example]]
|
||||
name = "ping"
|
||||
path = "examples/ping.rs"
|
@ -1,22 +0,0 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use env_logger::Env;
|
||||
use krata::dial::ControlDialAddress;
|
||||
use kratanet::NetworkService;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct NetworkArgs {
|
||||
#[arg(short, long, default_value = "unix:///var/lib/krata/daemon.socket")]
|
||||
connection: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
|
||||
let args = NetworkArgs::parse();
|
||||
let control_dial_address = ControlDialAddress::from_str(&args.connection)?;
|
||||
let mut service = NetworkService::new(control_dial_address).await?;
|
||||
service.watch().await
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
use std::{net::Ipv6Addr, str::FromStr, time::Duration};
|
||||
|
||||
use anyhow::Result;
|
||||
use kratanet::icmp::{IcmpClient, IcmpProtocol};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let client = IcmpClient::new(IcmpProtocol::Icmpv6)?;
|
||||
let payload: [u8; 4] = [12u8, 14u8, 16u8, 32u8];
|
||||
let result = client
|
||||
.ping6(
|
||||
Ipv6Addr::from_str("2606:4700:4700::1111")?,
|
||||
0,
|
||||
1,
|
||||
&payload,
|
||||
Duration::from_secs(10),
|
||||
)
|
||||
.await?;
|
||||
println!("reply: {:?}", result);
|
||||
Ok(())
|
||||
}
|
@ -1,198 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::{
|
||||
common::Zone,
|
||||
control::{
|
||||
control_service_client::ControlServiceClient, watch_events_reply::Event,
|
||||
ListZonesRequest,
|
||||
},
|
||||
},
|
||||
};
|
||||
use log::warn;
|
||||
use smoltcp::wire::{EthernetAddress, Ipv4Cidr, Ipv6Cidr};
|
||||
use std::{collections::HashMap, str::FromStr, time::Duration};
|
||||
use tokio::{select, sync::broadcast::Receiver, time::sleep};
|
||||
use tonic::transport::Channel;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct AutoNetworkWatcher {
|
||||
control: ControlServiceClient<Channel>,
|
||||
pub events: EventStream,
|
||||
known: HashMap<Uuid, NetworkMetadata>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NetworkSide {
|
||||
pub ipv4: Ipv4Cidr,
|
||||
pub ipv6: Ipv6Cidr,
|
||||
pub mac: EthernetAddress,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NetworkMetadata {
|
||||
pub domid: u32,
|
||||
pub uuid: Uuid,
|
||||
pub zone: NetworkSide,
|
||||
pub gateway: NetworkSide,
|
||||
}
|
||||
|
||||
impl NetworkMetadata {
|
||||
pub fn interface(&self) -> String {
|
||||
format!("vif{}.20", self.domid)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AutoNetworkChangeset {
|
||||
pub added: Vec<NetworkMetadata>,
|
||||
pub removed: Vec<NetworkMetadata>,
|
||||
}
|
||||
|
||||
impl AutoNetworkWatcher {
|
||||
pub async fn new(control: ControlServiceClient<Channel>) -> Result<AutoNetworkWatcher> {
|
||||
let client = control.clone();
|
||||
Ok(AutoNetworkWatcher {
|
||||
control,
|
||||
events: EventStream::open(client).await?,
|
||||
known: HashMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn read(&mut self) -> Result<Vec<NetworkMetadata>> {
|
||||
let mut all_zones: HashMap<Uuid, Zone> = HashMap::new();
|
||||
for zone in self
|
||||
.control
|
||||
.list_zones(ListZonesRequest {})
|
||||
.await?
|
||||
.into_inner()
|
||||
.zones
|
||||
{
|
||||
let Ok(uuid) = Uuid::from_str(&zone.id) else {
|
||||
continue;
|
||||
};
|
||||
all_zones.insert(uuid, zone);
|
||||
}
|
||||
|
||||
let mut networks: Vec<NetworkMetadata> = Vec::new();
|
||||
for (uuid, zone) in &all_zones {
|
||||
let Some(ref status) = zone.status else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if status.domid == u32::MAX {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(ref network_status) = status.network_status else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(zone_ipv4_cidr) = Ipv4Cidr::from_str(&network_status.zone_ipv4) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(zone_ipv6_cidr) = Ipv6Cidr::from_str(&network_status.zone_ipv6) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(zone_mac) = EthernetAddress::from_str(&network_status.zone_mac) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(gateway_ipv4_cidr) = Ipv4Cidr::from_str(&network_status.gateway_ipv4) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(gateway_ipv6_cidr) = Ipv6Cidr::from_str(&network_status.gateway_ipv6) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(gateway_mac) = EthernetAddress::from_str(&network_status.gateway_mac) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
networks.push(NetworkMetadata {
|
||||
domid: status.domid,
|
||||
uuid: *uuid,
|
||||
zone: NetworkSide {
|
||||
ipv4: zone_ipv4_cidr,
|
||||
ipv6: zone_ipv6_cidr,
|
||||
mac: zone_mac,
|
||||
},
|
||||
gateway: NetworkSide {
|
||||
ipv4: gateway_ipv4_cidr,
|
||||
ipv6: gateway_ipv6_cidr,
|
||||
mac: gateway_mac,
|
||||
},
|
||||
});
|
||||
}
|
||||
Ok(networks)
|
||||
}
|
||||
|
||||
pub async fn read_changes(&mut self) -> Result<AutoNetworkChangeset> {
|
||||
let mut seen: Vec<Uuid> = Vec::new();
|
||||
let mut added: Vec<NetworkMetadata> = Vec::new();
|
||||
let mut removed: Vec<NetworkMetadata> = Vec::new();
|
||||
|
||||
let networks = match self.read().await {
|
||||
Ok(networks) => networks,
|
||||
Err(error) => {
|
||||
warn!("failed to read network changes: {}", error);
|
||||
return Ok(AutoNetworkChangeset { added, removed });
|
||||
}
|
||||
};
|
||||
|
||||
for network in networks {
|
||||
seen.push(network.uuid);
|
||||
if self.known.contains_key(&network.uuid) {
|
||||
continue;
|
||||
}
|
||||
let _ = self.known.insert(network.uuid, network.clone());
|
||||
added.push(network);
|
||||
}
|
||||
|
||||
let mut gone: Vec<Uuid> = Vec::new();
|
||||
for uuid in self.known.keys() {
|
||||
if seen.contains(uuid) {
|
||||
continue;
|
||||
}
|
||||
gone.push(*uuid);
|
||||
}
|
||||
|
||||
for uuid in &gone {
|
||||
let Some(network) = self.known.remove(uuid) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
removed.push(network);
|
||||
}
|
||||
|
||||
Ok(AutoNetworkChangeset { added, removed })
|
||||
}
|
||||
|
||||
pub async fn wait(&mut self, receiver: &mut Receiver<Event>) -> Result<()> {
|
||||
loop {
|
||||
select! {
|
||||
x = receiver.recv() => match x {
|
||||
Ok(Event::ZoneChanged(_)) => {
|
||||
break;
|
||||
},
|
||||
|
||||
Err(error) => {
|
||||
warn!("failed to receive event: {}", error);
|
||||
}
|
||||
},
|
||||
|
||||
_ = sleep(Duration::from_secs(10)) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn mark_unknown(&mut self, uuid: Uuid) -> Result<bool> {
|
||||
Ok(self.known.remove(&uuid).is_some())
|
||||
}
|
||||
}
|
@ -1,177 +0,0 @@
|
||||
use crate::autonet::NetworkMetadata;
|
||||
use crate::chandev::ChannelDevice;
|
||||
use crate::nat::Nat;
|
||||
use crate::proxynat::ProxyNatHandlerFactory;
|
||||
use crate::raw_socket::{AsyncRawSocketChannel, RawSocketHandle, RawSocketProtocol};
|
||||
use crate::vbridge::{BridgeJoinHandle, VirtualBridge};
|
||||
use crate::EXTRA_MTU;
|
||||
use anyhow::{anyhow, Result};
|
||||
use bytes::BytesMut;
|
||||
use futures::TryStreamExt;
|
||||
use log::{info, trace, warn};
|
||||
use smoltcp::iface::{Config, Interface, SocketSet};
|
||||
use smoltcp::phy::Medium;
|
||||
use smoltcp::time::Instant;
|
||||
use smoltcp::wire::{HardwareAddress, IpCidr};
|
||||
use tokio::select;
|
||||
use tokio::sync::mpsc::{channel, Receiver};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
const TX_CHANNEL_BUFFER_LEN: usize = 3000;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NetworkBackend {
|
||||
metadata: NetworkMetadata,
|
||||
bridge: VirtualBridge,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum NetworkStackSelect {
|
||||
Receive(Option<BytesMut>),
|
||||
Send(Option<BytesMut>),
|
||||
}
|
||||
|
||||
struct NetworkStack<'a> {
|
||||
tx: Receiver<BytesMut>,
|
||||
kdev: AsyncRawSocketChannel,
|
||||
udev: ChannelDevice,
|
||||
interface: Interface,
|
||||
sockets: SocketSet<'a>,
|
||||
nat: Nat,
|
||||
bridge: BridgeJoinHandle,
|
||||
}
|
||||
|
||||
impl NetworkStack<'_> {
|
||||
async fn poll(&mut self) -> Result<bool> {
|
||||
let what = select! {
|
||||
biased;
|
||||
x = self.kdev.receiver.recv() => NetworkStackSelect::Receive(x),
|
||||
x = self.tx.recv() => NetworkStackSelect::Send(x),
|
||||
x = self.bridge.from_bridge_receiver.recv() => NetworkStackSelect::Send(x),
|
||||
x = self.bridge.from_broadcast_receiver.recv() => NetworkStackSelect::Send(x.ok()),
|
||||
};
|
||||
|
||||
match what {
|
||||
NetworkStackSelect::Receive(Some(packet)) => {
|
||||
if let Err(error) = self.bridge.to_bridge_sender.try_send(packet.clone()) {
|
||||
trace!("failed to send zone packet to bridge: {}", error);
|
||||
}
|
||||
|
||||
if let Err(error) = self.nat.receive_sender.try_send(packet.clone()) {
|
||||
trace!("failed to send zone packet to nat: {}", error);
|
||||
}
|
||||
|
||||
self.udev.rx = Some(packet);
|
||||
self.interface
|
||||
.poll(Instant::now(), &mut self.udev, &mut self.sockets);
|
||||
}
|
||||
|
||||
NetworkStackSelect::Send(Some(packet)) => {
|
||||
if let Err(error) = self.kdev.sender.try_send(packet) {
|
||||
warn!("failed to transmit packet to interface: {}", error);
|
||||
}
|
||||
}
|
||||
|
||||
NetworkStackSelect::Receive(None) | NetworkStackSelect::Send(None) => {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkBackend {
|
||||
pub fn new(metadata: NetworkMetadata, bridge: VirtualBridge) -> Result<Self> {
|
||||
Ok(Self { metadata, bridge })
|
||||
}
|
||||
|
||||
pub async fn init(&mut self) -> Result<()> {
|
||||
let interface = self.metadata.interface();
|
||||
let (connection, handle, _) = rtnetlink::new_connection()?;
|
||||
tokio::spawn(connection);
|
||||
|
||||
let mut links = handle.link().get().match_name(interface.clone()).execute();
|
||||
let link = links.try_next().await?;
|
||||
if link.is_none() {
|
||||
return Err(anyhow!(
|
||||
"unable to find network interface named {}",
|
||||
interface
|
||||
));
|
||||
}
|
||||
let link = link.unwrap();
|
||||
handle.link().set(link.header.index).up().execute().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run(&self) -> Result<()> {
|
||||
let mut stack = self.create_network_stack().await?;
|
||||
loop {
|
||||
if !stack.poll().await? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_network_stack(&self) -> Result<NetworkStack> {
|
||||
let interface = self.metadata.interface();
|
||||
let proxy = Box::new(ProxyNatHandlerFactory::new());
|
||||
let addresses: Vec<IpCidr> = vec![
|
||||
self.metadata.gateway.ipv4.into(),
|
||||
self.metadata.gateway.ipv6.into(),
|
||||
];
|
||||
let mut kdev =
|
||||
RawSocketHandle::bound_to_interface(&interface, RawSocketProtocol::Ethernet)?;
|
||||
let mtu = kdev.mtu_of_interface(&interface)? + EXTRA_MTU;
|
||||
let (tx_sender, tx_receiver) = channel::<BytesMut>(TX_CHANNEL_BUFFER_LEN);
|
||||
let mut udev = ChannelDevice::new(mtu, Medium::Ethernet, tx_sender.clone());
|
||||
let mac = self.metadata.gateway.mac;
|
||||
let local_cidrs = addresses.clone();
|
||||
let nat = Nat::new(mtu, proxy, mac, local_cidrs, tx_sender.clone())?;
|
||||
let hardware_addr = HardwareAddress::Ethernet(mac);
|
||||
let config = Config::new(hardware_addr);
|
||||
let mut iface = Interface::new(config, &mut udev, Instant::now());
|
||||
iface.update_ip_addrs(|addrs| {
|
||||
addrs
|
||||
.extend_from_slice(&addresses)
|
||||
.expect("failed to set ip addresses");
|
||||
});
|
||||
let sockets = SocketSet::new(vec![]);
|
||||
let handle = self.bridge.join(self.metadata.zone.mac).await?;
|
||||
let kdev = AsyncRawSocketChannel::new(mtu, kdev)?;
|
||||
Ok(NetworkStack {
|
||||
tx: tx_receiver,
|
||||
kdev,
|
||||
udev,
|
||||
interface: iface,
|
||||
sockets,
|
||||
nat,
|
||||
bridge: handle,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn launch(self) -> Result<JoinHandle<()>> {
|
||||
Ok(tokio::task::spawn(async move {
|
||||
info!(
|
||||
"launched network backend for krata zone {}",
|
||||
self.metadata.uuid
|
||||
);
|
||||
if let Err(error) = self.run().await {
|
||||
warn!(
|
||||
"network backend for krata zone {} failed: {}",
|
||||
self.metadata.uuid, error
|
||||
);
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for NetworkBackend {
|
||||
fn drop(&mut self) {
|
||||
info!(
|
||||
"destroyed network backend for krata zone {}",
|
||||
self.metadata.uuid
|
||||
);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user