diff --git a/Cargo.lock b/Cargo.lock index 2a573c2..a7b25ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -668,6 +668,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link", ] @@ -2934,6 +2935,22 @@ dependencies = [ "url", ] +[[package]] +name = "odoroboctl" +version = "0.1.0" +dependencies = [ + "bytesize", + "chrono", + "clap", + "odorobo", + "reqwest", + "serde", + "serde_json", + "stable-eyre", + "tokio", + "ulid", +] + [[package]] name = "oid-registry" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index ce70611..50129f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["odorobo"] +members = ["odorobo", "odoroboctl"] [workspace.package] rust-version = "1.95.0" @@ -18,6 +18,8 @@ tracing-subscriber = { version = "0.3", features = [ "fmt", "json", ] } +ulid = { version = "1.2", features = ["serde", "uuid"] } +bytesize = { version = "2.3", features = ["serde"] } # our crates diff --git a/README.md b/README.md index c2f7e24..8b05fc8 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ sudo dnf in -y clang-devel nftables cloud-hypervisor cargo build --release # Run the Agent & Manager (requires write permissions to /run/odorobo) -sudo ./target/release/odorobo --manager-enabled=true # or set ODOROBO_MANAGER_ENABLED=true +sudo ./target/release/odorobo --manager-enabled # or set ODOROBO_MANAGER_ENABLED=true # Run on other boxes sudo ./target/release/odorobo diff --git a/odorobo/Cargo.toml b/odorobo/Cargo.toml index 91cee6c..70a6407 100644 --- a/odorobo/Cargo.toml +++ b/odorobo/Cargo.toml @@ -13,6 +13,8 @@ serde_json = { workspace = true } serde = { workspace = true, features = ["derive"] } tracing = { workspace = true } tracing-subscriber = { workspace = true } +ulid = { workspace = true } +bytesize = { workspace = true } http-body-util = "0.1" hyper = { version = "1.9", features = ["full"] } @@ -39,9 +41,7 @@ axum-extra = "0.12" tower-http = { version = "0.6", features = ["trace"] } kameo = { version = "0.20", features = ["remote"] } ahash = { version = "0.8", features = ["serde"] } -ulid = { version = "1.2", features = ["serde", "uuid"] } dirs = "6" -bytesize = { version = "2.3", features = ["serde"] } ipnet = {version = "2.12", features = ["serde", "schemars08"]} url = { version = "2.5", features = ["serde"] } async-trait = "0.1" diff --git a/odorobo/src/actors/agent_actor.rs b/odorobo/src/actors/agent_actor.rs index 8eb694a..3745c2d 100644 --- a/odorobo/src/actors/agent_actor.rs +++ b/odorobo/src/actors/agent_actor.rs @@ -1,7 +1,6 @@ -use crate::{config::Config, types::ObjectMetadata, messages::{Ping, Pong, agent::{AgentStatus, GetAgentStatus}, debug::PanicAgent, vm::*}, networking::actor::NetworkAgentActor, state::provisioning::actor::VMActor, utils::actor_names::{NETWORK, VM, vm_actor_id}}; +use crate::{ch_driver::actor::VMActor, config::Config, messages::{Ping, Pong, agent::{AgentStatus, GetAgentStatus}, debug::PanicAgent, vm::*}, networking::actor::NetworkAgentActor, types::{ObjectMetadata, VirtualMachine}, utils::actor_names::{NETWORK, VM, vm_actor_id}}; use ahash::AHashMap; use bytesize::ByteSize; -use cloud_hypervisor_client::models::VmConfig; use kameo::prelude::*; use stable_eyre::{Report, Result}; use std::ops::ControlFlow; @@ -13,7 +12,7 @@ use kameo::error::PanicError; pub struct VMCacheData { actor_ref: ActorRef, - config: VmConfig + config: VirtualMachine } #[derive(RemoteActor)] @@ -137,7 +136,7 @@ impl Message for AgentActor { vmid, VMCacheData { actor_ref: actor_ref.clone(), - config: msg.config.clone() + config: Default::default() } ); @@ -285,12 +284,12 @@ impl Message for AgentActor { ) -> Self::Reply { let vcpus_used_by_vms = self.vms.values() - .map(|vm| vm.config.cpus.as_ref().map(|cpu_config| cpu_config.boot_vcpus).unwrap_or(0)) + .map(|vm| vm.config.data.vcpus) .reduce(|acc, cpus| acc + cpus) .unwrap_or(0) as u32; let ram_used_by_vms = self.vms.values() - .map(|vm| vm.config.memory.as_ref().map(|memory_config| memory_config.size).unwrap_or(0)) + .map(|vm| vm.config.data.memory.as_u64()) .reduce(|acc, memory| acc + memory) .unwrap_or(0) as u64; diff --git a/odorobo/src/actors/http_actor.rs b/odorobo/src/actors/http_actor.rs index a7d3f73..53f9ec6 100644 --- a/odorobo/src/actors/http_actor.rs +++ b/odorobo/src/actors/http_actor.rs @@ -1,4 +1,3 @@ -use cloud_hypervisor_client::models::{CpusConfig, MemoryConfig, PayloadConfig, VmConfig}; use kameo::prelude::*; use crate::messages::vm::{ AgentListVMs, AgentListVMsReply, CreateVM, CreateVMReply, DeleteVM, DeleteVMReply, @@ -6,8 +5,6 @@ use crate::messages::vm::{ }; use stable_eyre::{Report, Result}; -use crate::types::CreateVMRequest; - use super::scheduler_actor::SchedulerActor; const EXTERNAL_HTTP_ADDRESS: &str = "0.0.0.0:3000"; @@ -17,34 +14,6 @@ pub struct HTTPActor { pub scheduler: ActorRef, } -impl HTTPActor { - pub fn create_vm_message(request: CreateVMRequest) -> CreateVM { - CreateVM { - vmid: request.data.id, - config: VmConfig { - cpus: Some(CpusConfig { - boot_vcpus: request.data.vcpus as i32, - max_vcpus: request - .data - .max_vcpus - .map(|v| v as i32) - .unwrap_or(request.data.vcpus as i32), - ..Default::default() - }), - memory: Some(MemoryConfig { - size: request.data.memory.as_u64() as i64, - ..Default::default() - }), - payload: PayloadConfig { - kernel: Some(request.data.image), - ..Default::default() - }, - ..Default::default() - }, - } - } -} - impl Actor for HTTPActor { type Args = ActorRef; type Error = Report; diff --git a/odorobo/src/actors/scheduler_actor.rs b/odorobo/src/actors/scheduler_actor.rs index 3e97ab8..11d6a62 100644 --- a/odorobo/src/actors/scheduler_actor.rs +++ b/odorobo/src/actors/scheduler_actor.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use kameo::prelude::*; use libp2p::futures::TryStreamExt; use crate::actors::agent_actor::AgentActor; -use crate::state::provisioning::actor::VMActor; +use crate::ch_driver::actor::VMActor; use crate::utils::actor_cache::ActorCache; use crate::utils::actor_cache::ActorCacheUpdater; use crate::utils::actor_names::VM; diff --git a/odorobo/src/ch_api/ch.rs b/odorobo/src/ch_api/ch.rs deleted file mode 100644 index 6a081cd..0000000 --- a/odorobo/src/ch_api/ch.rs +++ /dev/null @@ -1,39 +0,0 @@ -use axum::{ - body::Body, - extract::{Path, Request}, - response::Response, -}; -use http_body_util::BodyExt; - -use super::error::ApiError; -use crate::state::VMInstance; - -pub async fn passthrough( - Path((vmid, path)): Path<(String, String)>, - request: Request, -) -> Result { - let vm = VMInstance::get(&vmid).ok_or_else(|| ApiError::VmNotFound { vmid: vmid.clone() })?; - - let (mut parts, body) = request.into_parts(); - let body = body - .collect() - .await - .map_err(|e| ApiError::PassthroughFailed { msg: e.to_string() })? - .to_bytes(); - - let path_and_query = match parts.uri.query() { - Some(query) => format!("/{path}?{query}"), - None => format!("/{path}"), - }; - parts.uri = path_and_query - .parse() - .map_err(|e| ApiError::PassthroughFailed { msg: format!("URI parse error: {}", e) })?; - - let response = vm - .call_request(hyper::Request::from_parts(parts, body)) - .await - .map_err(|e| ApiError::PassthroughFailed { msg: e.to_string() })?; - - let (parts, body) = response.into_parts(); - Ok(Response::from_parts(parts, Body::from(body))) -} diff --git a/odorobo/src/ch_api/console.rs b/odorobo/src/ch_api/console.rs deleted file mode 100644 index 5ccf9b5..0000000 --- a/odorobo/src/ch_api/console.rs +++ /dev/null @@ -1,259 +0,0 @@ -use axum::{ - extract::{ - Path, - ws::{Message, WebSocket, WebSocketUpgrade}, - }, - response::Response, -}; -use futures_util::SinkExt; -use serde::{Deserialize, Serialize}; -use stable_eyre::{Result, eyre::eyre}; -use std::io::{self, Read, Write}; -use std::os::fd::AsRawFd; -use tokio::io::unix::AsyncFd; -use tokio::time::{Duration, sleep}; -use tracing::{debug, trace, warn}; - -use super::error::ApiError; -use crate::state::{ConsoleStream, VMInstance}; - -pub async fn console_stream( - vmid: Path, - ws: WebSocketUpgrade, -) -> Result { - let vmid = vmid.0; - let vm = VMInstance::get(&vmid).ok_or_else(|| ApiError::VmNotFound { vmid: vmid.clone() })?; - let console = vm - .open_console() - .await - .map_err(|e| ApiError::ConsoleFailed { msg: e.to_string() })?; - - Ok(ws.on_upgrade(move |socket| proxy_console_socket(vmid, socket, console))) -} - -#[derive(Debug, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -enum ConsoleControlMessage { - Resize { - cols: u16, - rows: u16, - #[serde(default)] - x_pixels: u16, - #[serde(default)] - y_pixels: u16, - }, - ResetSession, -} - -#[derive(Debug, Serialize)] -#[serde(tag = "type", rename_all = "snake_case")] -enum ConsoleServerMessage<'a> { - Connected { vm_id: &'a str }, - Error { message: &'a str }, -} - -async fn proxy_console_socket(vm_id: String, mut socket: WebSocket, console: ConsoleStream) { - if let Err(err) = send_console_event( - &mut socket, - ConsoleServerMessage::Connected { vm_id: &vm_id }, - ) - .await - { - warn!(vm_id, ?err, "Failed to send connected event"); - return; - } - - let console_fd = match AsyncFd::new(console) { - Ok(fd) => fd, - Err(err) => { - warn!(vm_id, ?err, "Failed to wrap console in AsyncFd"); - return; - } - }; - - match proxy_console(&mut socket, console_fd).await { - Ok(()) => debug!(vm_id, "Console websocket disconnected"), - Err(err) => warn!(vm_id, ?err, "Console websocket proxy failed"), - } - - let _ = socket.close().await; -} - -async fn proxy_console(socket: &mut WebSocket, mut console: AsyncFd) -> Result<()> { - let mut buf = [0_u8; 8192]; - - loop { - // trace!("select waiting on ws.recv() and console.read()"); - tokio::select! { - message = socket.recv() => { - // trace!("ws.recv() returned: {:?}", message.as_ref().map(|m| match m { - // Ok(Message::Binary(d)) => format!("Binary({}b)", d.len()), - // Ok(Message::Text(t)) => format!("Text({}b)", t.len()), - // Ok(Message::Close(_)) => "Close".into(), - // Ok(Message::Ping(_)) => "Ping".into(), - // Ok(Message::Pong(_)) => "Pong".into(), - // Err(e) => format!("Error: {}", e), - // })); - match message { - Some(Ok(Message::Binary(data))) => { - // trace!(bytes = data.len(), "ws -> pty (binary)"); - let _ = console.writable().await?; - let data_copy = data.to_vec(); - let write_result = tokio::task::block_in_place(|| { - console.get_mut().write_all(&data_copy)?; - console.get_mut().flush()?; - Ok::<(), io::Error>(()) - }); - write_result?; - // trace!("write and flush done"); - } - Some(Ok(Message::Text(text))) => { - // trace!(len = text.len(), "ws -> pty (text/control)"); - handle_console_control(socket, &mut console, text.to_string()).await? - } - Some(Ok(Message::Close(_))) | None => { - // trace!("ws close or none"); - break; - } - Some(Ok(Message::Ping(payload))) => { - // trace!("ws ping"); - socket.send(Message::Pong(payload)).await? - } - Some(Ok(Message::Pong(_))) => { - // trace!("ws pong"); - } - Some(Err(err)) => { - trace!(?err, "ws error"); - return Err(err.into()); - } - } - } - read_result = async { - let _ = console.readable().await?; - tokio::task::block_in_place(|| { - console.get_mut().read(&mut buf) - }) - } => { - match read_result { - Ok(n) => { - if n == 0 { - // trace!("pty read returned 0 bytes"); - break; - } - // trace!(bytes = n, "pty -> ws"); - socket.send(Message::Binary(buf[..n].to_vec().into())).await?; - } - Err(e) if e.kind() == io::ErrorKind::WouldBlock => { - // trace!("pty read would block, retrying"); - // Continue select loop to try again - } - Err(e) => return Err(e.into()), - } - } - } - } - - Ok(()) -} - -async fn handle_console_control( - socket: &mut WebSocket, - console: &mut AsyncFd, - raw_message: String, -) -> Result<()> { - let message: ConsoleControlMessage = match serde_json::from_str(&raw_message) { - Ok(message) => message, - Err(_) => { - send_console_event( - socket, - ConsoleServerMessage::Error { - message: "Text frames are reserved for JSON control messages such as {\"type\":\"resize\",\"cols\":120,\"rows\":40} or {\"type\":\"reset_session\"}", - }, - ) - .await?; - return Ok(()); - } - }; - - match message { - ConsoleControlMessage::Resize { - cols, - rows, - x_pixels, - y_pixels, - } => { - if let Err(err) = resize_console(console, cols, rows, x_pixels, y_pixels) { - let message = format!("Failed to resize console: {err}"); - send_console_event(socket, ConsoleServerMessage::Error { message: &message }) - .await?; - } - } - ConsoleControlMessage::ResetSession => { - if let Err(err) = reset_console_session(console).await { - let message = format!("Failed to reset console session: {err}"); - send_console_event(socket, ConsoleServerMessage::Error { message: &message }) - .await?; - } - } - } - - Ok(()) -} - -async fn send_console_event(socket: &mut WebSocket, event: ConsoleServerMessage<'_>) -> Result<()> { - let payload = serde_json::to_string(&event)?; - socket.send(Message::Text(payload.into())).await?; - Ok(()) -} - -fn resize_console( - console: &AsyncFd, - cols: u16, - rows: u16, - x_pixels: u16, - y_pixels: u16, -) -> Result<()> { - if cols == 0 || rows == 0 { - return Err(eyre!("Console dimensions must be greater than zero")); - } - - let size = libc::winsize { - ws_row: rows, - ws_col: cols, - ws_xpixel: x_pixels, - ws_ypixel: y_pixels, - }; - - let result = unsafe { libc::ioctl(console.as_raw_fd(), libc::TIOCSWINSZ, &size) }; - if result == -1 { - return Err(std::io::Error::last_os_error().into()); - } - - Ok(()) -} - -async fn reset_console_session(console: &mut AsyncFd) -> Result<()> { - const STEP_DELAY: Duration = Duration::from_millis(75); - - macro_rules! write_to_console { - ($data:expr) => {{ - let _ = console.writable().await?; - let data = $data.to_vec(); - tokio::task::block_in_place(|| { - console.get_mut().write_all(&data)?; - console.get_mut().flush()?; - Ok::<(), io::Error>(()) - })?; - }}; - } - - write_to_console!(&[0x03]); - sleep(STEP_DELAY).await; - - write_to_console!(b"\r"); - sleep(STEP_DELAY).await; - - write_to_console!(&[0x04]); - - Ok(()) -} diff --git a/odorobo/src/ch_api/error.rs b/odorobo/src/ch_api/error.rs deleted file mode 100644 index ab53845..0000000 --- a/odorobo/src/ch_api/error.rs +++ /dev/null @@ -1,99 +0,0 @@ -use axum_responses::{HttpError, thiserror::Error}; -use stable_eyre::Report; - -use crate::state::ChApiError; - -#[derive(Debug, Error, HttpError)] -pub enum ApiError { - #[error("Invalid VM ID: {msg}")] - #[http(code = 400, message = msg)] - InvalidVmId { msg: String }, - - #[error("VM not found: {vmid}")] - #[http(code = 404, message = vmid)] - VmNotFound { vmid: String }, - - #[error("Failed to get VM info: {msg}")] - #[http(code = 500, message = msg, errors = errors)] - VmInfoFailed { msg: String, errors: Vec }, - - #[error("Failed to list VMs: {msg}")] - #[http(code = 500, message = msg)] - ListFailed { msg: String }, - - #[error("Failed to create VM: {msg}")] - #[http(code = 500, message = msg, errors = errors)] - CreateFailed { msg: String, errors: Vec }, - - #[error("Failed to open VM console: {msg}")] - #[http(code = 500, message = msg)] - ConsoleFailed { msg: String }, - - #[error("Failed to proxy Cloud Hypervisor API request: {msg}")] - #[http(code = 500, message = msg)] - PassthroughFailed { msg: String }, - - #[error("Cloud Hypervisor API error: {msg}")] - #[http(code = 502, message = msg, errors = errors)] - ChApiFailed { msg: String, errors: Vec }, - - #[error("Failed to delete VM configuration: configuration: {msg}")] - #[http(code = 500, message = msg, errors = errors)] - DeleteConfigFailed { msg: String, errors: Vec }, - - #[error("Failed to delete VM configuration: configuration: {msg}")] - #[http(code = 500, message = msg, errors = errors)] - CreateConfigFailed { msg: String, errors: Vec }, - - #[error("Failed to migrate VM: {msg}")] - #[http(code = 500, message = msg, errors = errors)] - MigrationFailed { msg: String, errors: Vec }, -} - -impl ApiError { - fn find_ch_api_error(error: &Report) -> Option<&ChApiError> { - error - .chain() - .find_map(|cause| cause.downcast_ref::()) - } - - fn from_report(error: Report, fallback: impl FnOnce(String, Vec) -> Self) -> Self { - if let Some(ch_error) = Self::find_ch_api_error(&error) { - return match ch_error { - ChApiError::Api { status, errors } => fallback( - format!("Cloud Hypervisor API returned status {}", status.as_u16()), - errors.clone(), - ), - ChApiError::Client(_) => fallback(format!("{error:?}"), vec![]), - }; - } - - fallback(format!("{error:?}"), vec![]) - } - - pub fn vm_info(error: Report) -> Self { - Self::from_report(error, |msg, errors| Self::VmInfoFailed { msg, errors }) - } - - pub fn create(error: Report) -> Self { - Self::from_report(error, |msg, errors| Self::CreateFailed { msg, errors }) - } - - pub fn create_config(error: Report) -> Self { - Self::from_report(error, |msg, errors| Self::CreateConfigFailed { - msg, - errors, - }) - } - - pub fn delete_config(error: Report) -> Self { - Self::from_report(error, |msg, errors| Self::DeleteConfigFailed { - msg, - errors, - }) - } - - pub fn migration(error: Report) -> Self { - Self::from_report(error, |msg, errors| Self::MigrationFailed { msg, errors }) - } -} diff --git a/odorobo/src/ch_api/mod.rs b/odorobo/src/ch_api/mod.rs deleted file mode 100644 index 3b9576c..0000000 --- a/odorobo/src/ch_api/mod.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! REST Management API for odorobo-agent -mod ch; -mod console; -mod error; -mod vm; - -// pub fn router(port: u16) -> axum::Router<()> { -// let info_route = axum::Router::new() -// .route("/info", axum::routing::get(info)) -// .with_state(port); - -// axum::Router::new() -// .layer( -// TraceLayer::new_for_http() -// .on_request(DefaultOnRequest::new()) -// .on_response(DefaultOnResponse::new()) -// ) -// .route("/", axum::routing::get(root)) -// .route("/health", axum::routing::get(health)) -// .merge(info_route) -// .nest("/vms", vm::router()) -// } - -// async fn root() -> &'static str { -// env!("CARGO_PKG_VERSION") -// } - -// async fn health() -> &'static str { -// "" -// } - -// #[derive(Serialize)] -// struct AgentInfo { -// version: &'static str, -// listening_addresses: Vec, -// } - -// async fn info(State(port): State) -> Json { -// let listening_addresses = if_addrs::get_if_addrs() -// .unwrap_or_default() -// .into_iter() -// .filter(|i| !i.is_loopback()) -// .map(|i| format!("{}:{}", i.ip(), port)) -// .collect(); -// Json(AgentInfo { -// version: env!("CARGO_PKG_VERSION"), -// listening_addresses, -// }) -// } diff --git a/odorobo/src/ch_api/vm.rs b/odorobo/src/ch_api/vm.rs deleted file mode 100644 index 85b9a6d..0000000 --- a/odorobo/src/ch_api/vm.rs +++ /dev/null @@ -1,250 +0,0 @@ - - -// pub fn router() -> axum::Router<()> { -// axum::Router::new() -// .route("/", axum::routing::get(list_vms)) -// // .route("/{vmid}", axum::routing::put(spawn_vm)) -// // .route("/{vmid}/config", axum::routing::put(create_vm_config)) -// // .route("/{vmid}/config", axum::routing::delete(delete_vm_config)) -// // .route("/{vmid}/shutdown", axum::routing::put(shutdown_vm)) -// // .route("/{vmid}/acpi_shutdown", axum::routing::put(shutdown_acpi)) -// // .route("/{vmid}/boot", axum::routing::put(boot_vm)) -// // .route("/{vmid}/pause", axum::routing::put(pause_vm)) -// // .route("/{vmid}/resume", axum::routing::put(resume_vm)) -// // .route("/{vmid}/migrate/send", axum::routing::put(migrate_send_vm)) -// // .route( -// // "/{vmid}/migrate/receive", -// // axum::routing::put(migrate_receive_vm), -// // ) -// // .route("/{vmid}", axum::routing::get(vm_info)) -// // .route("/{vmid}/ping", axum::routing::get(ping_vm)) -// // .route("/{vmid}", axum::routing::delete(destroy_vm)) -// // .route( -// // "/{vmid}/console", -// // axum::routing::get(super::console::console_stream), -// // ) -// // .route( -// // "/{vmid}/ch/{*path}", -// // axum::routing::any(super::ch::passthrough), -// // ) -// } - -// /// Lists all VMs by their IDs -// async fn list_vms() -> Result>, ApiError> { -// let vms = VMInstance::list().map_err(|e| ApiError::ListFailed { msg: e.to_string() })?; -// Ok(Json(vms.into_iter().map(|i| i.id).collect())) -// } - -// /// Helper function to get a VM instance by ID, returning an error if not found -// fn get_vm(vmid: &str) -> Result { -// use crate::state::VMInstance; -// VMInstance::validate_vmid(vmid).map_err(|e| ApiError::InvalidVmId { msg: e.to_string() })?; -// VMInstance::get(vmid).ok_or_else(|| ApiError::VmNotFound { -// vmid: vmid.to_string(), -// }) -// } - -// /// Gets detailed information about a specific VM -// async fn vm_info( -// vmid: Path, -// ) -> Result, ApiError> { -// let vm = get_vm(&vmid.0)?; - -// let info = vm.info().await.map_err(ApiError::vm_info)?; -// Ok(Json(info)) -// } - -// /// Pings the VMM to check if it's running -// async fn ping_vm(vmid: Path) -> Result, ApiError> { -// let vm = get_vm(&vmid.0)?; -// let res = vm.ping().await.map_err(ApiError::vm_info)?; -// Ok(Json(res)) -// } - -// #[derive(Debug, Deserialize)] -// pub struct CreateVmQuery { -// #[serde(default)] -// /// Whether to boot the VM immediately -// /// after creation -// /// -// /// Defaults to `false`. -// pub boot: bool, -// } -// #[derive(Debug, Deserialize, Serialize)] -// pub struct VmSpawnResponse { -// pub info: Option, -// pub booted: bool, -// pub created_config: bool, -// } - -// #[derive(Debug, Deserialize, Serialize)] -// pub struct VmMigrateSendResponse { -// pub info: Option, -// } -// #[derive(Debug, Deserialize, Serialize)] -// pub struct VmMigrateReceiveResponse { -// pub listening_address: String, -// } - -// #[derive(Debug, Deserialize)] -// pub struct VmMigrateSendRequest { -// pub destination: String, -// #[serde(default)] -// pub local: bool, -// } - -// /// Sends a live migration to the given destination URL. -// /// Note: the source VMM exits after migration completes, so no VM info is returned. -// async fn migrate_send_vm( -// vmid: Path, -// Json(body): Json, -// ) -> Result, ApiError> { -// let vm = get_vm(&vmid.0)?; -// vm.send_migration(&body.destination, body.local) -// .await -// .map_err(ApiError::migration)?; -// Ok(Json(VmMigrateSendResponse { info: None })) -// } - -// /// Prepares a VM to receive a live migration, returning the address the sender should connect to -// async fn migrate_receive_vm( -// vmid: Path, -// ) -> Result, ApiError> { -// let vm = get_vm(&vmid.0)?; -// let listening_address = vm.receive_migration().await.map_err(ApiError::migration)?; -// Ok(Json(VmMigrateReceiveResponse { listening_address })) -// } - -// /// Spawns a new VM instance with the given ID, optionally creating it with the provided configuration and booting it immediately -// async fn spawn_vm( -// vmid: Path, -// Query(query): Query, -// vm_config: Option>, -// ) -> Result, ApiError> { -// VMInstance::validate_vmid(&vmid.0).map_err(|e| ApiError::InvalidVmId { msg: e.to_string() })?; -// let vm_config = vm_config.map(|Json(vm_config)| vm_config); - -// // check if VM already exists, if so error out for already existing instance -// if VMInstance::get(&vmid.0).is_some() { -// error!(vmid = ?vmid, "VM with this ID already exists"); -// return Err(ApiError::CreateFailed { -// msg: "VM with this ID already exists".to_string(), -// errors: vec![], -// }); -// } - -// trace!(?vmid, ?query, "Creating VM with config"); -// // trace!(?vm_config, "VM config details"); -// let runtime_dir = VMInstance::runtime_dir_for(&vmid.0); -// std::fs::create_dir_all(&runtime_dir).map_err(|e| { -// error!(error = %e, "Failed to create runtime dir"); -// ApiError::CreateFailed { -// msg: e.to_string(), -// errors: vec![], -// } -// })?; -// // trace!(?) - -// let vm = VMInstance::spawn(&vmid.0).await.map_err(|e| { -// error!(error = ?e, "Failed to spawn VM process"); -// ApiError::create(e) -// })?; - -// let mut created = false; -// if vm_config.is_some() { -// trace!(?vmid, "Creating VM with provided config"); -// vm.create(vm_config.clone().unwrap(), query.boot) -// .await -// .map_err(|e| { -// error!(error = ?e, "Failed to create VM"); -// ApiError::create_config(e) -// })?; - -// created = true; -// } else { -// trace!(?vmid, "No VM config provided, skipping creation step"); -// } - -// let vm_info = if vm_config.is_some() { -// Some(vm.info().await.map_err(|e| { -// error!(error = ?e, "Failed to get VM info after creation"); -// ApiError::vm_info(e) -// })?) -// } else { -// None -// }; - -// Ok(Json(VmSpawnResponse { -// info: vm_info, -// booted: query.boot, -// created_config: created, -// })) -// } - -// async fn create_vm_config( -// vmid: Path, -// Query(query): Query, -// Json(vm_config): Json, -// ) -> Result, ApiError> { -// let vm = get_vm(&vmid.0)?; -// vm.create(vm_config, query.boot) -// .await -// .map_err(ApiError::create_config)?; -// Ok(Json(())) -// } - -// async fn delete_vm_config(vmid: Path) -> Result, ApiError> { -// let vm = get_vm(&vmid.0)?; -// vm.delete_config().await.map_err(ApiError::delete_config)?; -// Ok(Json(())) -// } - -// /// Forces a VM to shut down immediately, without giving it a chance to gracefully clean up resources. -// /// This is equivalent to pulling the power on a physical machine and may lead to data loss or corruption if the VM is running. -// /// -// /// The VM process itself will still be running until the VMM detects that the VM has stopped, -// /// not fully cleaning up resources. -// async fn shutdown_vm(vmid: Path) -> Result, ApiError> { -// let vm = get_vm(&vmid.0)?; -// vm.shutdown().await.map_err(ApiError::vm_info)?; -// Ok(Json(())) -// } - -// /// Sends an ACPI shutdown signal to the VM, allowing it to gracefully shut down and clean up resources. -// /// -// /// With the systemd provisioner, this will also fully clean up resources and destroy the VM instance entirely, -// /// allowing them to be re-provisioned again on any other node (if running in a cluster) -// async fn shutdown_acpi(vmid: Path) -> Result, ApiError> { -// let vm = get_vm(&vmid.0)?; -// vm.acpi_power_button().await.map_err(ApiError::vm_info)?; -// Ok(Json(())) -// } - -// /// Boots a VM that has been created but not yet started. If the VM is already running, this will return an error. -// async fn boot_vm(vmid: Path) -> Result, ApiError> { -// let vm = get_vm(&vmid.0)?; -// vm.boot().await.map_err(ApiError::vm_info)?; -// Ok(Json(())) -// } - -// /// Suspends a running VM, pausing all activity until -// /// it is resumed again. -// async fn pause_vm(vmid: Path) -> Result, ApiError> { -// let vm = get_vm(&vmid.0)?; -// vm.pause().await.map_err(ApiError::vm_info)?; -// Ok(Json(())) -// } - -// /// Resumes a paused VM, allowing it to continue running from where it left off. -// async fn resume_vm(vmid: Path) -> Result, ApiError> { -// let vm = get_vm(&vmid.0)?; -// vm.resume().await.map_err(ApiError::vm_info)?; -// Ok(Json(())) -// } - -// /// Destroys a VM, stopping it if it's running and cleaning up resources -// async fn destroy_vm(vmid: Path) -> Result, ApiError> { -// let vm = get_vm(&vmid.0)?; -// vm.destroy().await.map_err(ApiError::vm_info)?; -// Ok(Json(())) -// } diff --git a/odorobo/src/state/provisioning/actor/mod.rs b/odorobo/src/ch_driver/actor.rs similarity index 82% rename from odorobo/src/state/provisioning/actor/mod.rs rename to odorobo/src/ch_driver/actor.rs index ad32cbf..4586c87 100644 --- a/odorobo/src/state/provisioning/actor/mod.rs +++ b/odorobo/src/ch_driver/actor.rs @@ -1,5 +1,5 @@ -use crate::state::VMInstance; -use cloud_hypervisor_client::models::VmConfig; +use crate::{ch_driver::VMInstance, types::VirtualMachine}; +use cloud_hypervisor_client::models::{CpuFeatures, CpusConfig, DiskConfig, ImageType, MemoryConfig, NetConfig, PayloadConfig, PlatformConfig, VmConfig}; use kameo::prelude::*; use crate::messages::vm::{ DeleteVM, GetVMInfo, GetVMInfoReply, MigrateVMReceive, MigrateVMReceiveReply, PrepMigration, @@ -9,14 +9,7 @@ use serde::{Deserialize, Serialize}; use stable_eyre::{Report, Result}; use tokio::task::JoinHandle; use tracing::{debug, error, info, trace, warn}; -/* -use std::process::Command; - -let output = Command::new("echo") -.arg("Hello world") -.output() -.expect("Failed to execute command"); - */ + /// A migration state that holds the listening address and VM config for a migration, /// used to pass live migration data between actors. pub struct MigrationState { @@ -39,12 +32,12 @@ pub struct VMActor { impl Actor for VMActor { // tuple of VM ID and optional config - type Args = (ulid::Ulid, Option); + type Args = (ulid::Ulid, Option); type Error = Report; #[tracing::instrument(skip_all)] async fn on_start((vmid, vm_config): Self::Args, actor_ref: ActorRef) -> Result { - let mut vminstance = VMInstance::spawn(&vmid.to_string(), vm_config, None).await?; + let mut vminstance = VMInstance::spawn(&vmid.to_string(), vm_config.map(VmConfig::from), None).await?; // Take the child process out so we can watch for unexpected death. // destroy() handles a missing child_process gracefully. @@ -107,6 +100,46 @@ impl Actor for VMActor { } } +// todo: improve a lot of these config options. most of them should be set by the manifest +impl From for VmConfig { + fn from(vm: VirtualMachine) -> Self { + VmConfig { + cpus: Some(CpusConfig { + boot_vcpus: vm.data.vcpus as i32, + max_vcpus: vm.data.max_vcpus.unwrap_or(vm.data.vcpus) as i32, + ..Default::default() + }), + memory: Some(MemoryConfig { + size: vm.data.memory.as_u64() as i64, + ..Default::default() + }), + payload: PayloadConfig { + firmware: Some("/var/lib/odorobo/CLOUDHV.fd".to_string()), + ..Default::default() + }, + disks: Some(vec![ + DiskConfig { // todo: get cappy to make this auto generate this via the manifest's volumes atribute. + path: Some(vm.data.image), + image_type: Some(ImageType::Raw), + ..Default::default() + } + ]), + // todo: generate from VM network field + // net: Some(vec![ + // NetConfig { + // id: Some("net://devnet".to_string()), + // ..Default::default() + // } + // ]), + platform: Some(PlatformConfig { + serial_number: Some("ds=nocloud".to_string()), + ..Default::default() + }), + ..Default::default() + } + } +} + // allow conversion from VMActor to VMInstance to call API impl From for VMInstance { fn from(actor: VMActor) -> Self { diff --git a/odorobo/src/state/api.rs b/odorobo/src/ch_driver/api.rs similarity index 100% rename from odorobo/src/state/api.rs rename to odorobo/src/ch_driver/api.rs diff --git a/odorobo/src/state/devices.rs b/odorobo/src/ch_driver/devices.rs similarity index 96% rename from odorobo/src/state/devices.rs rename to odorobo/src/ch_driver/devices.rs index 5558eb8..2d02a0c 100644 --- a/odorobo/src/state/devices.rs +++ b/odorobo/src/ch_driver/devices.rs @@ -1,4 +1,4 @@ -use crate::state::VMInstance; +use crate::ch_driver::VMInstance; use cloud_hypervisor_client::{ apis::DefaultApi, models::{DeviceConfig, NetConfig, PciDeviceInfo, VmRemoveDevice}, diff --git a/odorobo/src/state/instance.rs b/odorobo/src/ch_driver/instance.rs similarity index 99% rename from odorobo/src/state/instance.rs rename to odorobo/src/ch_driver/instance.rs index ecbf347..8433f7e 100644 --- a/odorobo/src/state/instance.rs +++ b/odorobo/src/ch_driver/instance.rs @@ -17,7 +17,7 @@ use thiserror::Error; use tokio::task::JoinHandle; use tracing::{debug, error, info, trace, warn}; -use crate::state::{ +use crate::ch_driver::{ provisioning::hooks::HookManager, transform::{ConfigTransform, TransformChain}, }; diff --git a/odorobo/src/state/mod.rs b/odorobo/src/ch_driver/mod.rs similarity index 65% rename from odorobo/src/state/mod.rs rename to odorobo/src/ch_driver/mod.rs index 00fa350..8701f5b 100644 --- a/odorobo/src/state/mod.rs +++ b/odorobo/src/ch_driver/mod.rs @@ -8,6 +8,6 @@ pub mod devices; pub mod instance; pub mod provisioning; pub mod transform; +pub mod actor; -pub use instance::{ChApiError, ConsoleStream, VMInstance}; -// pub use transform::{ConfigTransform, ConsoleTransform, TransformChain, apply_builtin_transforms}; +pub use instance::{VMInstance}; diff --git a/odorobo/src/state/provisioning/hooks/mod.rs b/odorobo/src/ch_driver/provisioning/hooks/mod.rs similarity index 100% rename from odorobo/src/state/provisioning/hooks/mod.rs rename to odorobo/src/ch_driver/provisioning/hooks/mod.rs diff --git a/odorobo/src/state/provisioning/hooks/networking.rs b/odorobo/src/ch_driver/provisioning/hooks/networking.rs similarity index 100% rename from odorobo/src/state/provisioning/hooks/networking.rs rename to odorobo/src/ch_driver/provisioning/hooks/networking.rs diff --git a/odorobo/src/state/provisioning/mod.rs b/odorobo/src/ch_driver/provisioning/mod.rs similarity index 93% rename from odorobo/src/state/provisioning/mod.rs rename to odorobo/src/ch_driver/provisioning/mod.rs index be490cc..24bdf32 100644 --- a/odorobo/src/state/provisioning/mod.rs +++ b/odorobo/src/ch_driver/provisioning/mod.rs @@ -2,5 +2,4 @@ //! //! Provides helper functions that calls the necessary hooks and various methods to start a //! Cloud Hypervisor process for a given instance -pub mod actor; pub mod hooks; diff --git a/odorobo/src/state/transform/console.rs b/odorobo/src/ch_driver/transform/console.rs similarity index 98% rename from odorobo/src/state/transform/console.rs rename to odorobo/src/ch_driver/transform/console.rs index b76711a..e675f06 100644 --- a/odorobo/src/state/transform/console.rs +++ b/odorobo/src/ch_driver/transform/console.rs @@ -2,7 +2,7 @@ use cloud_hypervisor_client::models::{ConsoleConfig, VmConfig}; use stable_eyre::Result; use tracing::trace; -use crate::state::VMInstance; +use crate::ch_driver::VMInstance; use super::ConfigTransform; diff --git a/odorobo/src/state/transform/mod.rs b/odorobo/src/ch_driver/transform/mod.rs similarity index 100% rename from odorobo/src/state/transform/mod.rs rename to odorobo/src/ch_driver/transform/mod.rs diff --git a/odorobo/src/state/transform/networking.rs b/odorobo/src/ch_driver/transform/networking.rs similarity index 100% rename from odorobo/src/state/transform/networking.rs rename to odorobo/src/ch_driver/transform/networking.rs diff --git a/odorobo/src/state/transform/path_verify.rs b/odorobo/src/ch_driver/transform/path_verify.rs similarity index 100% rename from odorobo/src/state/transform/path_verify.rs rename to odorobo/src/ch_driver/transform/path_verify.rs diff --git a/odorobo/src/state/transform/storage/file.rs b/odorobo/src/ch_driver/transform/storage/file.rs similarity index 100% rename from odorobo/src/state/transform/storage/file.rs rename to odorobo/src/ch_driver/transform/storage/file.rs diff --git a/odorobo/src/state/transform/storage/iscsi.rs b/odorobo/src/ch_driver/transform/storage/iscsi.rs similarity index 100% rename from odorobo/src/state/transform/storage/iscsi.rs rename to odorobo/src/ch_driver/transform/storage/iscsi.rs diff --git a/odorobo/src/state/transform/storage/mod.rs b/odorobo/src/ch_driver/transform/storage/mod.rs similarity index 99% rename from odorobo/src/state/transform/storage/mod.rs rename to odorobo/src/ch_driver/transform/storage/mod.rs index 1021465..1e04735 100644 --- a/odorobo/src/state/transform/storage/mod.rs +++ b/odorobo/src/ch_driver/transform/storage/mod.rs @@ -1,4 +1,4 @@ -use crate::state::transform::ConfigTransform; +use crate::ch_driver::transform::ConfigTransform; use async_trait::async_trait; use cloud_hypervisor_client::models::VmConfig; use stable_eyre::Result; diff --git a/odorobo/src/state/transform/storage/rbd.rs b/odorobo/src/ch_driver/transform/storage/rbd.rs similarity index 100% rename from odorobo/src/state/transform/storage/rbd.rs rename to odorobo/src/ch_driver/transform/storage/rbd.rs diff --git a/odorobo/src/http_api/vms.rs b/odorobo/src/http_api/vms.rs index 672614b..6e6b2fb 100644 --- a/odorobo/src/http_api/vms.rs +++ b/odorobo/src/http_api/vms.rs @@ -2,7 +2,7 @@ use crate::{ actors::http_actor::HTTPActor, types::{ - CreateVMRequest, DebugCreateVMRequest, UpdateVMRequest, VMData, VirtualMachine, VMListResponse, VMStatus, VmId + CreateVMRequest, UpdateVMRequest, VirtualMachine, VMListResponse, VmId }, messages::vm::CreateVM, utils::OdoroboError, }; use aide::axum::{ @@ -20,8 +20,6 @@ pub fn router() -> ApiRouter> { ApiRouter::new() .api_route("/", get(list_vms)) .api_route("/", post(create_vm)) - // undocumented debug route, do not use in prod - .route("/", axum::routing::put(debug_create_vm)) .api_route("/{vmid}", get(vm_info)) .api_route("/{vmid}", patch(update_vm)) .api_route("/{vmid}", delete(delete_vm)) @@ -49,39 +47,14 @@ async fn create_vm( State(state): State>, Json(request): Json, ) -> Result { - let vm_data = request.data.clone(); - let message = HTTPActor::create_vm_message(request); - - let _reply = state.ask(message).await?; - - Ok(Json(VirtualMachine { - data: vm_data, - node: None, - status: VMStatus::Provisioning, - ..Default::default() - })) -} - -async fn debug_create_vm( - State(state): State>, - Json(request): Json, -) -> Result { - let ulid = ulid::Ulid::new(); let message = CreateVM { - vmid: ulid, - config: request.vm_config, + vmid: request.vm.data.id, + config: request.vm, }; - let _reply = state.ask(message).await?; + let reply = state.ask(message).await?; - Ok(Json(VirtualMachine { - status: VMStatus::Provisioning, - data: VMData { - id: ulid, - ..Default::default() - }, - ..Default::default() - })) + Ok(Json(reply)) } async fn delete_vm( diff --git a/odorobo/src/lib.rs b/odorobo/src/lib.rs new file mode 100644 index 0000000..cd40856 --- /dev/null +++ b/odorobo/src/lib.rs @@ -0,0 +1 @@ +pub mod types; diff --git a/odorobo/src/main.rs b/odorobo/src/main.rs index 350843f..cd8dd39 100644 --- a/odorobo/src/main.rs +++ b/odorobo/src/main.rs @@ -1,8 +1,7 @@ pub mod actors; -mod ch_api; pub mod http_api; pub mod networking; -mod state; +mod ch_driver; mod utils; pub mod messages; pub mod config; diff --git a/odorobo/src/messages/vm.rs b/odorobo/src/messages/vm.rs index a1dd566..512a793 100644 --- a/odorobo/src/messages/vm.rs +++ b/odorobo/src/messages/vm.rs @@ -1,9 +1,12 @@ //! VM-related messages use cloud_hypervisor_client::models::VmConfig; use kameo::prelude::*; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use ulid::Ulid; +use crate::types::VirtualMachine; + // TODO: when scheduler does createVM it also stores which server we put the Ulid on so it can do a in memory cache and doesn't need to hit the Server // for failover, the new node when it fails over will need to rebuild this cache via hitting a GetAllVMs message on every server // additionally, when the VmConfig is created, this determines the MAC address of the server. meaning as soon as we have this info, we need to hit the router via the scheduler, because the router might be slow. @@ -20,12 +23,12 @@ pub struct CreateVM { /// node-specific, paths, i.e attach LUNs, networking? /// /// this data would go to state::instance::spawn() - pub config: VmConfig, + pub config: VirtualMachine, } -#[derive(Serialize, Deserialize, Reply, Debug)] +#[derive(Serialize, Deserialize, Reply, Debug, JsonSchema)] pub struct CreateVMReply { - pub config: Option, + pub config: Option, } /// Message to delete a VM's config from the agent, shutting it down diff --git a/odorobo/src/types.rs b/odorobo/src/types.rs index 8e10a25..b526cf8 100644 --- a/odorobo/src/types.rs +++ b/odorobo/src/types.rs @@ -66,7 +66,7 @@ impl Default for StorageUri { #[derive(Serialize, Deserialize, Debug, JsonSchema, Default, Clone)] pub struct CreateVMRequest { /// Data of the VM to create - pub data: VMData, + pub vm: VirtualMachine, /// Whether to boot the VM immediately after creation pub boot: bool, } @@ -105,6 +105,9 @@ pub struct VMData { /// List of volumes to attach to the VM. #[serde(default)] pub volumes: Vec, + /// List of Network IDs. + #[serde(default)] + pub networks: Vec, } #[derive(Serialize, Deserialize, Debug, JsonSchema, Default)] @@ -124,7 +127,7 @@ pub struct UpdateVMRequest { pub volumes: Vec, } -#[derive(Serialize, Deserialize, Debug, JsonSchema, Default)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Default, Clone)] pub enum VMStatus { /// VM is currently running and operational. Running, @@ -136,7 +139,7 @@ pub enum VMStatus { Error(String), // error message } -#[derive(Serialize, Deserialize, Debug, JsonSchema, Default)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Default, Clone)] pub struct ObjectMetadata { /// Labels associated with the object. pub labels: BTreeMap, @@ -146,7 +149,7 @@ pub struct ObjectMetadata { /// Detailed information about a running VM // probably move this somewhere else -#[derive(Serialize, Deserialize, Debug, JsonSchema, Default)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Default, Clone)] pub struct VirtualMachine { /// VM configuration pub data: VMData, @@ -163,8 +166,59 @@ pub struct VirtualMachine { /// Metadata pub metadata: Option, - // placement stuff.... - + /// List of Affinity rules for scheduling. These are ANDed / summed together depending on the strictness. + pub affinity: Option> +} + + +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] +pub struct AffinityRule { + pub strictness: AffinityStrictness, + pub affinity_type: AffinityType, + pub direction: AffinityDirection, + /// ORed together + pub requirements: Vec +} + +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] +pub enum AffinityStrictness { + Required, + Preferred { weight: i64 } +} + +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] +pub enum AffinityType { + VirtualMachine, + Agent +} + +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] +pub enum AffinityDirection { + Normal, + Anti +} + +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] +pub struct AffinityRequirement { + pub key: String, + pub table: MetadataTable, + pub operator: Operator, + pub values: Vec +} + +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] +pub enum MetadataTable { + Label, + Annotation +} + +// todo: possibly replace with std::ops +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] +pub enum Operator { + In, + NotIn, + Lt, + Gt, } #[derive(Serialize, Deserialize, Debug, JsonSchema, Default)] diff --git a/odoroboctl/Cargo.toml b/odoroboctl/Cargo.toml index 22be64c..75119ec 100644 --- a/odoroboctl/Cargo.toml +++ b/odoroboctl/Cargo.toml @@ -11,4 +11,9 @@ tokio = { workspace = true, features = ["full"] } reqwest = { workspace = true, features = ["json"] } serde_json = { workspace = true } serde = { workspace = true, features = ["derive"] } +ulid = { workspace = true } +bytesize = { workspace = true } + chrono = { version = "0.4", features = ["serde"] } + +odorobo = { workspace = true } diff --git a/odoroboctl/src/cli.rs b/odoroboctl/src/cli.rs index 98bdbf0..b769271 100644 --- a/odoroboctl/src/cli.rs +++ b/odoroboctl/src/cli.rs @@ -1,9 +1,10 @@ -use std::path::PathBuf; - use clap::{Parser, Subcommand}; use reqwest::{Client, Response}; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize}; use stable_eyre::Result; +use odorobo::types::{CreateVMRequest, VMData, VirtualMachine}; +use ulid::Ulid; +use bytesize::ByteSize; #[derive(Parser)] #[command( @@ -25,17 +26,9 @@ pub struct Cli { #[derive(Subcommand)] pub enum Command { - /// Create a VM via the scheduler debug endpoint, + /// Create a VM via the scheduler endpoint, /// optionally also booting it immediately after creation (if `--boot` is specified). - Create { - /// Path to the VM config file - /// (in Cloud Hypervisor JSON format) - config: PathBuf, - - /// Boot the VM after creation - #[arg(long)] - boot: bool, - }, + Create, /// List VMs currently known by the manager/agent. List, @@ -53,12 +46,6 @@ pub enum Command { }, } -#[derive(Debug, Serialize)] -struct DebugCreateVMRequest { - vm_config: serde_json::Value, - boot: bool, -} - #[derive(Debug, Deserialize)] #[serde(transparent)] struct VmId(String); @@ -109,12 +96,29 @@ pub async fn run_command(cli: Cli) -> Result<()> { let base_url = cli.manager_addr; match cli.command { - Command::Create { config, boot } => { + Command::Create => { // TODO: setup actual cli args for these parameters. or just take in arbitrary json and serialize it into a VirtualMachine. + let vm = VirtualMachine { + data: VMData { + id: Ulid::new(), + name: "test_vm".to_string(), + vcpus: 4, + max_vcpus: None, + memory: ByteSize::gib(4), + image: "/var/lib/odorobo/f43.raw".to_string(), + ..Default::default() + }, + ..Default::default() + }; + + let request = CreateVMRequest { + vm, + boot: true + }; + let url = format!("{}/vms", base_url); - let vm_config = - serde_json::from_str::(&std::fs::read_to_string(&config)?)?; - let body = DebugCreateVMRequest { vm_config, boot }; - let response = client.put(&url).json(&body).send().await?; + let response = client.post(&url).json(&request).send().await?; + + println!("{:?}", response.url()); print_message_response(response, "VM create request sent successfully").await?; }