diff --git a/.gitignore b/.gitignore index cf82bef..cf495b1 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,8 @@ dev # everyone keeps their own LLM configs AGENTS.md CLAUDE.md -.claude/ \ No newline at end of file +.claude/ + + +# mac bullshit +.DS_Store \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 7c5eaa3..39e4c77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,7 +59,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", + "getrandom 0.3.4", "once_cell", + "serde", "version_check", "zerocopy", ] @@ -380,6 +382,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" dependencies = [ "axum-core", + "axum-macros", "base64 0.22.1", "bytes", "form_urlencoded", @@ -453,6 +456,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "axum_responses" version = "0.5.5" @@ -1131,6 +1145,15 @@ dependencies = [ "syn", ] +[[package]] +name = "env" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc95de49ad098572c02d3fbf368c9a020bfff5ae78483685b77f51d8a7e9486d" +dependencies = [ + "num_threads", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -2783,6 +2806,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "object" version = "0.32.2" @@ -2822,6 +2854,39 @@ dependencies = [ "zbus_systemd", ] +[[package]] +name = "odorobo-scheduler" +version = "0.1.0" +dependencies = [ + "ahash", + "axum", + "env", + "kameo", + "libp2p", + "odorobo-shared", + "optional_struct", + "reqwest", + "serde", + "serde_json", + "tokio", + "tower-http", + "tracing", + "tracing-subscriber", + "utoipa", + "uuid", +] + +[[package]] +name = "odorobo-shared" +version = "0.1.0" +dependencies = [ + "kameo", + "libp2p", + "serde", + "tokio", + "utoipa", +] + [[package]] name = "odoroboctl" version = "0.1.0" @@ -2873,6 +2938,27 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +[[package]] +name = "optional_struct" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14199f59efce6ed2c5854f0abc725c32eedfbd02c6ef82c9733c726f3fc6dc91" +dependencies = [ + "optional_struct_macro", + "serde", +] + +[[package]] +name = "optional_struct_macro" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5eba042d9efe5e108e0df9ce2f85c540fc4f94f41c6821cbcf70ed47c1221da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -3745,15 +3831,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shared" -version = "0.1.0" -dependencies = [ - "kameo", - "libp2p", - "tokio", -] - [[package]] name = "shlex" version = "1.3.0" @@ -4388,6 +4465,30 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "utoipa" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" +dependencies = [ + "indexmap", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "uuid", +] + [[package]] name = "uuid" version = "1.23.0" diff --git a/Cargo.toml b/Cargo.toml index 900a991..14e4921 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["odorobo-agent", "odoroboctl", "shared"] +members = ["odorobo-agent", "odoroboctl", "odorobo-scheduler", "odorobo-shared"] [workspace.dependencies] stable-eyre = "0.2.2" diff --git a/odorobo-scheduler/Cargo.toml b/odorobo-scheduler/Cargo.toml new file mode 100644 index 0000000..30dcff9 --- /dev/null +++ b/odorobo-scheduler/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "odorobo-scheduler" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "odorobo-scheduler" + +[[bin]] +name = "gen_openapi_spec" + +[dependencies] +kameo = { version = "0.19.2", features = ["remote"] } +axum = { version = "0.8.8", features = ["tracing", "macros"] } +env = "1.0.1" +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.149" +tokio = { version = "1.50.0", features = ["full"] } # TODO (june): tracing is only in tokio unstable? NOTE (caleb): I think that just refers to tokio logging tracing events, we can still make our own and look at other crate's events if necessary. +tower-http = { version = "0.6.1", features = ["trace"] } +tracing = "0.1.44" +optional_struct = "0.5.2" +tracing-subscriber = { version = "0.3.2", features = ["env-filter"] } +uuid = { version = "1.23.0", features = ["serde"]} +libp2p = { version = "0.56.0", features = ["yamux", "serde", "mdns"] } +reqwest = { version = "0.13", features = ["json"] } +ahash = { version = "0.8.12", features = ["serde"] } +utoipa = { version = "5.4.0", features=["uuid"] } +odorobo-shared = { path = "../odorobo-shared" } \ No newline at end of file diff --git a/odorobo-scheduler/src/bin/gen_openapi_spec.rs b/odorobo-scheduler/src/bin/gen_openapi_spec.rs new file mode 100644 index 0000000..eb5f75d --- /dev/null +++ b/odorobo-scheduler/src/bin/gen_openapi_spec.rs @@ -0,0 +1,7 @@ +use std::fs; +use odorobo_scheduler::scheduler_actor::gen_openapi_spec; + +fn main() -> std::io::Result<()> { + let doc = gen_openapi_spec(); + fs::write("./openapi_spec.json", doc) +} \ No newline at end of file diff --git a/odorobo-scheduler/src/bin/odorobo-scheduler.rs b/odorobo-scheduler/src/bin/odorobo-scheduler.rs new file mode 100644 index 0000000..ad0b614 --- /dev/null +++ b/odorobo-scheduler/src/bin/odorobo-scheduler.rs @@ -0,0 +1,16 @@ +use kameo::prelude::*; +use odorobo_shared::utils::DynError; +use odorobo_scheduler::scheduler_actor::SchedulerActor; +use odorobo_shared::connect_to_swarm; + +#[tokio::main] +async fn main() -> Result<(), DynError> { + let _local_peer_id = connect_to_swarm()?; + + let actor_ref = SchedulerActor::spawn(SchedulerActor {}); + actor_ref.register("scheduler").await?; + + actor_ref.wait_for_shutdown().await; + + Ok(()) +} \ No newline at end of file diff --git a/odorobo-scheduler/src/lib.rs b/odorobo-scheduler/src/lib.rs new file mode 100644 index 0000000..e887c65 --- /dev/null +++ b/odorobo-scheduler/src/lib.rs @@ -0,0 +1,2 @@ + +pub mod scheduler_actor; \ No newline at end of file diff --git a/odorobo-scheduler/src/scheduler_actor.rs b/odorobo-scheduler/src/scheduler_actor.rs new file mode 100644 index 0000000..5eb85bb --- /dev/null +++ b/odorobo-scheduler/src/scheduler_actor.rs @@ -0,0 +1,246 @@ +use axum::{Json, Router}; +use axum::extract::State; +use axum::http::StatusCode; +use axum::routing::{get, post}; +use kameo::prelude::*; +use libp2p::futures::TryStreamExt; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; +use libp2p::PeerId; +use utoipa::OpenApi; +use odorobo_shared::kameo_messages::{ServerStatus, GetServerStatus}; +//use odorobo_shared::odorobo::server_actor::ServerActor; +use odorobo_shared::utils::DynError; + +#[derive(RemoteActor)] +pub struct SchedulerActor { } + +const PING_RETURN_VALUE: &str = "pong"; +const EXTERNAL_HTTP_ADDRESS: &str = "0.0.0.0:3000"; + +const EXTERNAL_HTTP_URL: &str = "http://localhost:3000"; // TODO: make this based on EXTERNAL_HTTP_ADDRESS. const compile time stuff is a pain. + + +impl Actor for SchedulerActor { + type Args = Self; + type Error = DynError; + + async fn on_start(state: Self::Args, actor_ref: ActorRef) -> Result { + let axum_router = Router::new() + .route("/ping", get(|| async { PING_RETURN_VALUE })) + .route("/create_vm", post(create_vm)) + .route("/delete_vm", post(delete_vm)) + .route("/update_vm", post(update_vm)) + .route("/get_vm", post(get_vm)) + .route("/drain_server", post(drain_server)) + .route("/get_servers", post(get_servers)) + .with_state(actor_ref); + + + println!("starting axum server at {}", EXTERNAL_HTTP_URL); + + // run our app with hyper, listening globally on port 3000 + tokio::spawn(async move { + let listener = tokio::net::TcpListener::bind(EXTERNAL_HTTP_ADDRESS).await.unwrap(); + axum::serve(listener, axum_router).await.unwrap(); + }); + + // spin loop until the axum server starts responding to requests + // TODO: if anyone has a better way to detect the axum server is up, change it to that. + + let mut count = 0; + loop { + count += 1; + println!("attempting to hit axum server, attempt {}", count); + + let resp_result: Result<(), DynError> = async { + let resp = reqwest::get(EXTERNAL_HTTP_URL.to_owned() + "/ping") + .await? + .text() + .await?; + + if resp != PING_RETURN_VALUE { + return Err("invalid ping response".into()); + } + + Ok(()) + }.await; + + match resp_result { + Ok(()) => { + break; + }, + Err(e) => { + println!("{}", e) + } + } + + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + + println!("Actor started"); + Ok(state) + } +} + +#[derive(Serialize, Deserialize, Debug, utoipa::ToSchema)] +pub struct CreateVM { + pub uuid: Uuid, + pub name: String, + pub vcpus: u32, + pub ram: u32, + pub image: String, +} + +#[derive(Serialize, Deserialize, Debug, utoipa::ToSchema)] +pub struct GenericSuccessResponse { + pub success: bool, +} + +// no response. just use status code 200 vs not 200 for if it worked. +#[utoipa::path( + post, + path = "/create_vm", + request_body(content = CreateVM, content_type = "application/json"), + responses( + (status = 200, body = GenericSuccessResponse) + ) +)] +pub async fn create_vm(State(state): State>, Json(payload): Json) -> (StatusCode, String) { + todo!() +} + +pub type UpdateVM = CreateVM; + +#[utoipa::path( + post, + path = "/update_vm", + request_body(content = UpdateVM, content_type = "application/json"), + responses( + (status = 200, body = GenericSuccessResponse) + ) +)] +async fn update_vm(State(state): State>, Json(payload): Json) -> (StatusCode, String) { + todo!() +} + + +pub type DeleteVM = GetVM; +#[utoipa::path( + post, + path = "/delete_vm", + request_body(content = DeleteVM, content_type = "application/json"), + responses( + (status = 200, body = GenericSuccessResponse) + ) +)] +async fn delete_vm(State(state): State>, Json(payload): Json) -> (StatusCode, String) { + todo!() +} + + +#[derive(Serialize, Deserialize, Debug, utoipa::ToSchema)] +pub struct GetVM { + pub uuid: Uuid, +} + +#[derive(Serialize, Deserialize, Debug, utoipa::ToSchema)] +pub struct GetVMResponse { + pub cpus: bool +} +#[utoipa::path( + post, + path = "/get_vm", + request_body(content = GetVM, content_type = "application/json"), + responses( + (status = 200, body = GetVMResponse) + ) +)] +async fn get_vm(State(state): State>, Json(payload): Json) -> (StatusCode, String) { + todo!() +} + +#[derive(Serialize, Deserialize, Debug, utoipa::ToSchema)] +pub struct DrainServer {} + +#[utoipa::path( + post, + path = "/get_vm", + request_body(content = DrainServer, content_type = "application/json"), + responses( + (status = 200, body = GenericSuccessResponse) + ) +)] +async fn drain_server(State(state): State>, Json(payload): Json) -> (StatusCode, String) { + todo!() +} + +#[derive(Serialize, Deserialize, Debug, utoipa::ToSchema)] +pub struct GetServers { + pub start_index: u64, + pub end_index: u64 +} + +#[derive(Serialize, Deserialize, Debug, utoipa::ToSchema)] +pub struct GetServersResponse { + pub total_servers: u64, + pub servers: Vec, +} + + +#[utoipa::path( + post, + path = "/get_servers", + request_body(content = GetServers, content_type = "application/json"), + responses( + (status = 200, body = GetServersResponse) + ) +)] +async fn get_servers(State(state): State>, Json(payload): Json) -> (StatusCode, String) { + /* + let mut servers: Vec<(PeerId, ServerStatus)> = Vec::new(); + + let server_actor_response: Result<(), DynError> = async { + println!("getting server actors"); + + let mut server_actors = RemoteActorRef::::lookup_all("server"); + + while let Some(server_actor) = server_actors.try_next().await? { + // Send message to each instance + println!("asking {:?}", server_actor); + let result = server_actor.ask(&GetServerStatus { }).await?; + println!("result {:?}", result); + + if let Some(peerId) = server_actor.id().peer_id() { + servers.push((*peerId, result)); + } + } + + Ok(()) + }.await; + + match server_actor_response { + Ok(()) => { + (StatusCode::OK, serde_json::to_string(&servers).unwrap()) + }, + _ => { + (StatusCode::INTERNAL_SERVER_ERROR, "".parse().unwrap()) + } + } + + */ + todo!() +} + +pub fn gen_openapi_spec() -> String { + #[derive(OpenApi)] + #[openapi( + components( + schemas(CreateVM, UpdateVM, DeleteVM, GetVM, DrainServer, GetServers, GenericSuccessResponse, GetVMResponse, GetServersResponse) + ), + paths(get_servers) + )] + struct ApiDoc; + + ApiDoc::openapi().to_pretty_json().unwrap() +} \ No newline at end of file diff --git a/shared/Cargo.toml b/odorobo-shared/Cargo.toml similarity index 67% rename from shared/Cargo.toml rename to odorobo-shared/Cargo.toml index cdfc97d..a0672b8 100644 --- a/shared/Cargo.toml +++ b/odorobo-shared/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "shared" +name = "odorobo-shared" version = "0.1.0" edition = "2024" license = "AGPL-3.0-or-later" @@ -8,3 +8,5 @@ license = "AGPL-3.0-or-later" kameo = { version = "0.19.2", features = ["remote"] } tokio = { version = "1.50.0", features = ["full"] } libp2p = { version = "0.56.0", features = ["yamux", "serde", "mdns"] } +serde = { version = "1.0.228", features = ["derive"] } +utoipa = { version = "5.4.0", features=["uuid"] } diff --git a/odorobo-shared/src/kameo_messages.rs b/odorobo-shared/src/kameo_messages.rs new file mode 100644 index 0000000..0e704bc --- /dev/null +++ b/odorobo-shared/src/kameo_messages.rs @@ -0,0 +1,13 @@ +use std::fmt::{Display, Formatter}; +use kameo::Reply; +use libp2p::PeerId; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct GetServerStatus {} + +#[derive(Serialize, Deserialize, Reply, Debug, utoipa::ToSchema)] +pub struct ServerStatus { + pub vcpus: u32, + pub ram: u32 +} \ No newline at end of file diff --git a/shared/src/lib.rs b/odorobo-shared/src/lib.rs similarity index 99% rename from shared/src/lib.rs rename to odorobo-shared/src/lib.rs index 01c5406..af9f1c4 100644 --- a/shared/src/lib.rs +++ b/odorobo-shared/src/lib.rs @@ -1,4 +1,5 @@ pub mod utils; +pub mod kameo_messages; use kameo::prelude::*; use libp2p::{mdns, noise, tcp, yamux, PeerId}; diff --git a/shared/src/utils.rs b/odorobo-shared/src/utils.rs similarity index 100% rename from shared/src/utils.rs rename to odorobo-shared/src/utils.rs