Skip to content

Commit

Permalink
feat(agent): add guest agent with rust support (#25)
Browse files Browse the repository at this point in the history
* feat(agent): add lib and rust agent

Signed-off-by: Martin Moreira de Jesus <[email protected]>

* chore: run cargo fmt

Signed-off-by: Martin Moreira de Jesus <[email protected]>

* chore: update action from example (so it can compile by default)

Signed-off-by: Martin Moreira de Jesus <[email protected]>

* refactor: move rust and Agent trait into agents module and AgentRunner into workload module

Signed-off-by: Matéo Fernandez <[email protected]>
Signed-off-by: Martin Moreira de Jesus <[email protected]>

* feat(agent): add status code, stderr and stdout to prepare and run

Co-authored-by: Matéo Fernandez <[email protected]>
Signed-off-by: Martin Moreira de Jesus <[email protected]>

---------

Signed-off-by: Martin Moreira de Jesus <[email protected]>
Signed-off-by: Matéo Fernandez <[email protected]>
Co-authored-by: Matéo Fernandez <[email protected]>
  • Loading branch information
mmoreiradj and mfernd authored Apr 23, 2024
1 parent 5b4c1be commit f176aa8
Show file tree
Hide file tree
Showing 10 changed files with 337 additions and 1 deletion.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[workspace]
members = ["src/api", "src/vmm", "src/cli", "src/fs-gen"]
members = ["src/api", "src/vmm", "src/cli", "src/fs-gen", "src/agent"]
resolver = "2"
14 changes: 14 additions & 0 deletions src/agent/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "agent"
version = "0.1.0"
edition = "2021"

[lib]
name = "agent"
path = "src/lib.rs"

[dependencies]
clap = { version = "4.5.4", features = ["derive"] }
rand = "0.8.5"
serde = { version = "1.0.197", features = ["derive"] }
toml = "0.8.12"
7 changes: 7 additions & 0 deletions src/agent/examples/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
workload-name = "fibonacci"
language = "rust"
action = "prepare-and-run"

[build]
source-code-path = "CHANGEME/cloudlet/src/agent/examples/main.rs"
release = true
21 changes: 21 additions & 0 deletions src/agent/examples/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
fn fibonacci(n: u32) -> u64 {
match n {
0 => 0,
1 => 1,
_ => {
let mut prev = 0;
let mut curr = 1;
for _ in 2..=n {
let next = prev + curr;
prev = curr;
curr = next;
}
curr
}
}
}

fn main() {
let n = 10;
println!("Fibonacci number at position {} is: {}", n, fibonacci(n));
}
30 changes: 30 additions & 0 deletions src/agent/src/agents/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use crate::AgentResult;
use serde::Deserialize;

pub mod rust;

#[derive(Debug, Clone)]
pub struct AgentOutput {
pub exit_code: i32,
pub stdout: String,
pub stderr: String,
}

pub trait Agent {
fn prepare(&self) -> AgentResult<AgentOutput>;
fn run(&self) -> AgentResult<AgentOutput>;
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Language {
Rust,
}

impl std::fmt::Display for Language {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Language::Rust => write!(f, "rust"),
}
}
}
152 changes: 152 additions & 0 deletions src/agent/src/agents/rust.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
use super::{Agent, AgentOutput};
use crate::{workload, AgentError, AgentResult};
use rand::distributions::{Alphanumeric, DistString};
use serde::Deserialize;
use std::{fs::create_dir_all, process::Command};

#[derive(Deserialize)]
#[serde(rename_all = "kebab-case")]
struct RustAgentBuildConfig {
release: bool,
source_code_path: String,
}

#[derive(Deserialize)]
struct RustAgentConfig {
build: RustAgentBuildConfig,
}

pub struct RustAgent {
workload_config: workload::config::Config,
rust_config: RustAgentConfig,
}

impl RustAgent {
fn build(&self, function_dir: &String) -> AgentResult<AgentOutput> {
if self.rust_config.build.release {
let output = Command::new("cargo")
.arg("build")
.arg("--release")
.current_dir(function_dir)
.output()
.expect("Failed to build function");

Ok(AgentOutput {
exit_code: output.status.code().unwrap(),
stdout: std::str::from_utf8(&output.stdout).unwrap().to_string(),
stderr: std::str::from_utf8(&output.stderr).unwrap().to_string(),
})
} else {
let output = Command::new("cargo")
.arg("build")
.current_dir(function_dir)
.output()
.expect("Failed to build function");

Ok(AgentOutput {
exit_code: output.status.code().unwrap(),
stdout: std::str::from_utf8(&output.stdout).unwrap().to_string(),
stderr: std::str::from_utf8(&output.stderr).unwrap().to_string(),
})
}
}
}

// TODO should change with a TryFrom
impl From<workload::config::Config> for RustAgent {
fn from(workload_config: workload::config::Config) -> Self {
let rust_config: RustAgentConfig = toml::from_str(&workload_config.config_string).unwrap();

Self {
workload_config,
rust_config,
}
}
}

impl Agent for RustAgent {
fn prepare(&self) -> AgentResult<AgentOutput> {
let code = std::fs::read_to_string(&self.rust_config.build.source_code_path).unwrap();

let function_dir = format!(
"/tmp/{}",
Alphanumeric.sample_string(&mut rand::thread_rng(), 16)
);

println!("Function directory: {}", function_dir);

create_dir_all(format!("{}/src", &function_dir)).expect("Unable to create directory");

std::fs::write(format!("{}/src/main.rs", &function_dir), code)
.expect("Unable to write main.rs file");

let cargo_toml = format!(
r#"
[package]
name = "{}"
version = "0.1.0"
edition = "2018"
"#,
self.workload_config.workload_name
);

std::fs::write(format!("{}/Cargo.toml", &function_dir), cargo_toml)
.expect("Unable to write Cargo.toml file");

let result = self.build(&function_dir)?;

if result.exit_code != 0 {
println!("Build failed: {:?}", result);
return Err(AgentError::BuildFailed(AgentOutput {
exit_code: result.exit_code,
stdout: result.stdout,
stderr: result.stderr,
}));
}

// Copy the binary to /tmp, we could imagine a more complex scenario where we would put this in an artifact repository (like S3)
let binary_path = match self.rust_config.build.release {
true => format!(
"{}/target/release/{}",
&function_dir, self.workload_config.workload_name
),
false => format!(
"{}/target/debug/{}",
&function_dir, self.workload_config.workload_name
),
};

std::fs::copy(
binary_path,
format!("/tmp/{}", self.workload_config.workload_name),
)
.expect("Unable to copy binary");

std::fs::remove_dir_all(&function_dir).expect("Unable to remove directory");

Ok(AgentOutput {
exit_code: result.exit_code,
stdout: "Build successful".to_string(),
stderr: "".to_string(),
})
}

fn run(&self) -> AgentResult<AgentOutput> {
let output = Command::new(format!("/tmp/{}", self.workload_config.workload_name))
.output()
.expect("Failed to run function");

let agent_output = AgentOutput {
exit_code: output.status.code().unwrap(),
stdout: std::str::from_utf8(&output.stdout).unwrap().to_string(),
stderr: std::str::from_utf8(&output.stderr).unwrap().to_string(),
};

if !output.status.success() {
println!("Run failed: {:?}", agent_output);
return Err(AgentError::BuildFailed(agent_output));
}

Ok(agent_output)
}
}
16 changes: 16 additions & 0 deletions src/agent/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use agents::AgentOutput;

mod agents;
pub mod workload {
pub mod config;
pub mod runner;
}

#[derive(Debug)]
pub enum AgentError {
OpenConfigFileError(std::io::Error),
ParseConfigError(toml::de::Error),
BuildFailed(AgentOutput),
}

pub type AgentResult<T> = Result<T, AgentError>;
18 changes: 18 additions & 0 deletions src/agent/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use agent::workload::{config::Config, runner::Runner};
use clap::Parser;
use std::path::PathBuf;

#[derive(Debug, Parser)]
struct Args {
#[clap(short, long, default_value = "/etc/cloudlet/agent/config.toml")]
config: PathBuf,
}

fn main() {
let args = Args::parse();

let config = Config::from_file(&args.config).unwrap();
let runner = Runner::new(config);

runner.run().unwrap();
}
40 changes: 40 additions & 0 deletions src/agent/src/workload/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use crate::{agents::Language, AgentError, AgentResult};
use serde::Deserialize;
use std::path::PathBuf;

/// Generic agent configuration.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Config {
/// Name of the worklod, used to identify the workload.
pub workload_name: String,
/// Language of the workload.
pub language: Language,
/// Action to perform.
pub action: Action,
/// Rest of the configuration as a string.
#[serde(skip)]
pub config_string: String,
}

impl Config {
pub fn from_file(file_path: &PathBuf) -> AgentResult<Self> {
let config = std::fs::read_to_string(file_path).map_err(AgentError::OpenConfigFileError)?;
let mut config: Config = toml::from_str(&config).map_err(AgentError::ParseConfigError)?;

let config_string =
std::fs::read_to_string(file_path).map_err(AgentError::OpenConfigFileError)?;

config.config_string = config_string;

Ok(config)
}
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Action {
Prepare,
Run,
PrepareAndRun,
}
38 changes: 38 additions & 0 deletions src/agent/src/workload/runner.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use super::config::{Action, Config};
use crate::{
agents::{rust, Agent, Language},
AgentResult,
};

/// Runner for a workload.
/// Will execute the workload based on the inner agent (language).
pub struct Runner {
config: Config,
agent: Box<dyn Agent>,
}

impl Runner {
pub fn new(config: Config) -> Self {
let agent: Box<dyn Agent> = match config.language {
Language::Rust => Box::new(rust::RustAgent::from(config.clone())),
};

Runner { config, agent }
}

pub fn run(&self) -> AgentResult<()> {
let result = match self.config.action {
Action::Prepare => self.agent.prepare()?,
Action::Run => self.agent.run()?,
Action::PrepareAndRun => {
let res = self.agent.prepare()?;
println!("Prepare result {:?}", res);
self.agent.run()?
}
};

println!("Result: {:?}", result);

Ok(())
}
}

0 comments on commit f176aa8

Please sign in to comment.