use chrono::{DateTime, Utc};
use rand::RngCore;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::fmt;
use std::hash::Hash;
pub use crate::litebox::{BoxState, BoxStatus, HealthStatus};
use crate::runtime::id::BoxID;
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[serde(transparent)]
pub struct Bytes(pub u64);
impl Bytes {
#[inline]
pub const fn from_bytes(bytes: u64) -> Self {
Self(bytes)
}
#[inline]
pub const fn from_kib(kib: u64) -> Self {
Self(kib * 1024)
}
#[inline]
pub const fn from_mib(mib: u64) -> Self {
Self(mib * 1024 * 1024)
}
#[inline]
pub const fn from_gib(gib: u64) -> Self {
Self(gib * 1024 * 1024 * 1024)
}
#[inline]
pub const fn as_bytes(&self) -> u64 {
self.0
}
#[inline]
pub const fn as_kib(&self) -> u64 {
self.0 / 1024
}
#[inline]
pub const fn as_mib(&self) -> u64 {
self.0 / (1024 * 1024)
}
}
impl From<u64> for Bytes {
fn from(bytes: u64) -> Self {
Self(bytes)
}
}
impl From<Bytes> for u64 {
fn from(b: Bytes) -> Self {
b.0
}
}
impl fmt::Display for Bytes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.0 >= 1024 * 1024 * 1024 && self.0.is_multiple_of(1024 * 1024 * 1024) {
write!(f, "{} GiB", self.0 / (1024 * 1024 * 1024))
} else if self.0 >= 1024 * 1024 && self.0.is_multiple_of(1024 * 1024) {
write!(f, "{} MiB", self.0 / (1024 * 1024))
} else if self.0 >= 1024 && self.0.is_multiple_of(1024) {
write!(f, "{} KiB", self.0 / 1024)
} else {
write!(f, "{} bytes", self.0)
}
}
}
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[serde(transparent)]
pub struct Seconds(pub u64);
impl Seconds {
#[inline]
pub const fn from_seconds(s: u64) -> Self {
Self(s)
}
#[inline]
pub const fn from_minutes(m: u64) -> Self {
Self(m * 60)
}
#[inline]
pub const fn from_hours(h: u64) -> Self {
Self(h * 60 * 60)
}
#[inline]
pub const fn as_seconds(&self) -> u64 {
self.0
}
#[inline]
pub const fn as_minutes(&self) -> u64 {
self.0 / 60
}
}
impl From<u64> for Seconds {
fn from(s: u64) -> Self {
Self(s)
}
}
impl From<Seconds> for u64 {
fn from(s: Seconds) -> Self {
s.0
}
}
impl fmt::Display for Seconds {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.0 >= 3600 && self.0.is_multiple_of(3600) {
write!(f, "{} hours", self.0 / 3600)
} else if self.0 >= 60 && self.0.is_multiple_of(60) {
write!(f, "{} minutes", self.0 / 60)
} else {
write!(f, "{} seconds", self.0)
}
}
}
#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ContainerID(String);
impl ContainerID {
pub const FULL_LENGTH: usize = 64;
pub const SHORT_LENGTH: usize = 12;
pub fn new() -> Self {
let mut random_bytes = [0u8; 32];
rand::rng().fill_bytes(&mut random_bytes);
let mut hasher = Sha256::new();
hasher.update(random_bytes);
let result = hasher.finalize();
Self(hex::encode(result))
}
pub fn parse(s: &str) -> Option<Self> {
if Self::is_valid(s) {
Some(Self(s.to_string()))
} else {
None
}
}
pub fn is_valid(s: &str) -> bool {
s.len() == Self::FULL_LENGTH
&& s.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_uppercase())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn short(&self) -> &str {
&self.0[..Self::SHORT_LENGTH]
}
}
impl Default for ContainerID {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for ContainerID {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl fmt::Debug for ContainerID {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ContainerID({})", self.short())
}
}
impl AsRef<str> for ContainerID {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BoxInfo {
pub id: BoxID,
pub name: Option<String>,
pub status: BoxStatus,
pub created_at: DateTime<Utc>,
pub last_updated: DateTime<Utc>,
pub pid: Option<u32>,
pub image: String,
pub cpus: u8,
pub memory_mib: u32,
pub labels: HashMap<String, String>,
pub health_status: HealthStatus,
}
impl BoxInfo {
pub fn new(config: &crate::litebox::config::BoxConfig, state: &BoxState) -> Self {
use crate::runtime::options::RootfsSpec;
Self {
id: config.id.clone(),
name: config.name.clone(),
status: state.status,
created_at: config.created_at,
last_updated: state.last_updated,
pid: state.pid,
image: match &config.options.rootfs {
RootfsSpec::Image(r) => r.clone(),
RootfsSpec::RootfsPath(p) => format!("rootfs:{}", p),
},
cpus: config.options.cpus.unwrap_or(2),
memory_mib: config.options.memory_mib.unwrap_or(512),
labels: HashMap::new(),
health_status: state.health_status,
}
}
}
impl PartialEq for BoxInfo {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
&& self.status == other.status
&& self.created_at == other.created_at
&& self.pid == other.pid
&& self.image == other.image
&& self.cpus == other.cpus
&& self.memory_mib == other.memory_mib
&& self.labels == other.labels
&& self.health_status == other.health_status
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BoxStateInfo {
pub status: BoxStatus,
pub running: bool,
pub pid: Option<u32>,
}
impl BoxStateInfo {
pub fn new(state: &BoxState) -> Self {
Self {
status: state.status,
running: state.status.is_running(),
pid: state.pid,
}
}
}
impl From<&BoxInfo> for BoxStateInfo {
fn from(info: &BoxInfo) -> Self {
Self {
status: info.status,
running: info.status.is_running(),
pid: info.pid,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageInfo {
pub reference: String,
pub repository: String,
pub tag: String,
pub id: String,
pub cached_at: DateTime<Utc>,
pub size: Option<Bytes>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::litebox::config::{BoxConfig, ContainerRuntimeConfig};
use crate::runtime::options::{BoxOptions, RootfsSpec};
use boxlite_shared::Transport;
use std::path::PathBuf;
#[test]
fn test_config_state_to_info() {
let now = Utc::now();
let box_id = BoxID::parse("01HJK4TNRPQSXYZ8WM6NCVT9R5").unwrap();
let config = BoxConfig {
id: box_id,
name: None,
created_at: now,
container: ContainerRuntimeConfig {
id: ContainerID::new(),
},
options: BoxOptions {
rootfs: RootfsSpec::Image("python:3.11".to_string()),
cpus: Some(4),
memory_mib: Some(1024),
..Default::default()
},
engine_kind: crate::vmm::VmmKind::Libkrun,
transport: Transport::unix(PathBuf::from("/tmp/boxlite.sock")),
box_home: PathBuf::from("/tmp/box"),
ready_socket_path: PathBuf::from("/tmp/ready.sock"),
};
let mut state = BoxState::new();
state.set_pid(Some(12345));
let _ = state.transition_to(BoxStatus::Running);
let info = BoxInfo::new(&config, &state);
assert_eq!(info.id, config.id);
assert_eq!(info.status, state.status);
assert_eq!(info.created_at, config.created_at);
assert_eq!(info.pid, state.pid);
assert_eq!(info.image, "python:3.11");
assert_eq!(info.cpus, 4);
assert_eq!(info.memory_mib, 1024);
}
#[test]
fn test_container_id_new() {
let id1 = ContainerID::new();
let id2 = ContainerID::new();
assert_eq!(id1.as_str().len(), ContainerID::FULL_LENGTH);
assert_eq!(id2.as_str().len(), ContainerID::FULL_LENGTH);
assert_ne!(id1, id2);
assert!(
id1.as_str()
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_uppercase())
);
}
#[test]
fn test_container_id_short() {
let id = ContainerID::new();
assert_eq!(id.short().len(), ContainerID::SHORT_LENGTH);
assert!(id.as_str().starts_with(id.short()));
}
#[test]
fn test_container_id_from_str() {
let valid = "a".repeat(64);
assert!(ContainerID::parse(&valid).is_some());
assert!(ContainerID::parse("abc123").is_none());
let uppercase = "A".repeat(64);
assert!(ContainerID::parse(&uppercase).is_none());
let non_hex = "g".repeat(64);
assert!(ContainerID::parse(&non_hex).is_none());
}
#[test]
fn test_container_id_display() {
let id = ContainerID::new();
let display = format!("{}", id);
assert_eq!(display, id.as_str());
}
#[test]
fn test_container_id_debug() {
let id = ContainerID::new();
let debug = format!("{:?}", id);
assert!(debug.contains(id.short()));
assert!(debug.starts_with("ContainerID("));
}
#[test]
fn test_bytes_constructors() {
assert_eq!(Bytes::from_bytes(1000).as_bytes(), 1000);
assert_eq!(Bytes::from_kib(1).as_bytes(), 1024);
assert_eq!(Bytes::from_mib(1).as_bytes(), 1024 * 1024);
assert_eq!(Bytes::from_gib(1).as_bytes(), 1024 * 1024 * 1024);
}
#[test]
fn test_bytes_conversions() {
let size = Bytes::from_mib(512);
assert_eq!(size.as_kib(), 512 * 1024);
assert_eq!(size.as_mib(), 512);
let odd_size = Bytes::from_bytes(1500);
assert_eq!(odd_size.as_kib(), 1); }
#[test]
fn test_bytes_from_u64() {
let b: Bytes = 1024u64.into();
assert_eq!(b.as_bytes(), 1024);
let raw: u64 = Bytes::from_mib(1).into();
assert_eq!(raw, 1024 * 1024);
}
#[test]
fn test_bytes_display() {
assert_eq!(format!("{}", Bytes::from_gib(2)), "2 GiB");
assert_eq!(format!("{}", Bytes::from_mib(512)), "512 MiB");
assert_eq!(format!("{}", Bytes::from_kib(64)), "64 KiB");
assert_eq!(format!("{}", Bytes::from_bytes(500)), "500 bytes");
assert_eq!(format!("{}", Bytes::from_bytes(1500)), "1500 bytes");
}
#[test]
fn test_bytes_ordering() {
assert!(Bytes::from_mib(1) < Bytes::from_mib(2));
assert!(Bytes::from_gib(1) > Bytes::from_mib(512));
}
#[test]
fn test_bytes_default() {
assert_eq!(Bytes::default().as_bytes(), 0);
}
#[test]
fn test_seconds_constructors() {
assert_eq!(Seconds::from_seconds(30).as_seconds(), 30);
assert_eq!(Seconds::from_minutes(5).as_seconds(), 300);
assert_eq!(Seconds::from_hours(1).as_seconds(), 3600);
}
#[test]
fn test_seconds_conversions() {
let duration = Seconds::from_hours(2);
assert_eq!(duration.as_minutes(), 120);
assert_eq!(duration.as_seconds(), 7200);
let odd_duration = Seconds::from_seconds(90);
assert_eq!(odd_duration.as_minutes(), 1); }
#[test]
fn test_seconds_from_u64() {
let s: Seconds = 300u64.into();
assert_eq!(s.as_seconds(), 300);
let raw: u64 = Seconds::from_minutes(5).into();
assert_eq!(raw, 300);
}
#[test]
fn test_seconds_display() {
assert_eq!(format!("{}", Seconds::from_hours(2)), "2 hours");
assert_eq!(format!("{}", Seconds::from_minutes(30)), "30 minutes");
assert_eq!(format!("{}", Seconds::from_seconds(45)), "45 seconds");
assert_eq!(format!("{}", Seconds::from_seconds(90)), "90 seconds");
}
#[test]
fn test_seconds_ordering() {
assert!(Seconds::from_seconds(30) < Seconds::from_minutes(1));
assert!(Seconds::from_hours(1) > Seconds::from_minutes(30));
}
#[test]
fn test_seconds_default() {
assert_eq!(Seconds::default().as_seconds(), 0);
}
}