Compare commits

..

7 Commits

Author SHA1 Message Date
65ee3231b2
switch to new staging brain 2025-06-18 17:46:52 +03:00
9630cd5f95
Brain redirect on app deploy and delete
Improve macro with full crate path for log debug
Simplifies brain URL selection by using lazy static variables for staging and testing environments.
2025-06-18 19:19:28 +05:30
25eeab6098
Refactor app resource disk size to GB
updated proto and changed accordingly
2025-06-16 15:47:53 +05:30
420653bcb6
Improves brain URL selection for different networks
list of 3 brain urls for staging and testnet
refactor brain URL selection for staging and testnet environments.
2025-06-16 13:53:10 +05:30
ddc5d24857
Implement follow redirect on delete and update vm 2025-06-13 17:21:52 +05:30
a25c53d709
Implement macro for grpc redirect
redirect handling in gRPC client and add macro for follow redirects
random brain url selection for staging environment.
cleanup duplicate sign_request function
2025-06-13 17:08:52 +05:30
41d9bd104f
WIP on implementing redirect
Refactors brain connection logic to use custom endpoint while runtime
Implements redirect handling in the `create_vm` function to gracefully handle server moves by reconnecting to the new URL
2025-06-11 19:12:14 +05:30
13 changed files with 160 additions and 62 deletions

2
Cargo.lock generated

@ -1182,7 +1182,7 @@ dependencies = [
[[package]] [[package]]
name = "detee-shared" name = "detee-shared"
version = "0.1.0" version = "0.1.0"
source = "git+ssh://git@gitea.detee.cloud/testnet/proto.git?branch=surreal_brain#fb38352e1b47837b14f32d8df5ae7f6b17202aae" source = "git+ssh://git@gitea.detee.cloud/testnet/proto.git?branch=surreal_brain_app#0b195b4589e4ec689af7ddca27dc051716ecee78"
dependencies = [ dependencies = [
"bincode", "bincode",
"prost", "prost",

@ -34,7 +34,7 @@ tokio-retry = "0.3.0"
detee-sgx = { git = "ssh://git@gitea.detee.cloud/testnet/detee-sgx.git", branch = "hratls", features=["hratls", "qvl"] } detee-sgx = { git = "ssh://git@gitea.detee.cloud/testnet/detee-sgx.git", branch = "hratls", features=["hratls", "qvl"] }
shadow-rs = { version = "1.1.1", features = ["metadata"] } shadow-rs = { version = "1.1.1", features = ["metadata"] }
detee-shared = { git = "ssh://git@gitea.detee.cloud/testnet/proto.git", branch = "surreal_brain" } detee-shared = { git = "ssh://git@gitea.detee.cloud/testnet/proto.git", branch = "surreal_brain_app" }
# detee-shared = { path = "../detee-shared" } # detee-shared = { path = "../detee-shared" }
[build-dependencies] [build-dependencies]

@ -127,9 +127,9 @@ fn clap_cmd() -> Command {
.arg( .arg(
Arg::new("disk") Arg::new("disk")
.long("disk") .long("disk")
.default_value("1000") .default_value("2")
.value_parser(clap::value_parser!(u32).range(300..8000)) .value_parser(clap::value_parser!(u32).range(1..100))
.help("disk size in MB") .help("disk size in GB")
) )
.arg( .arg(
Arg::new("port") Arg::new("port")

@ -1,3 +1,4 @@
use crate::constants::{BRAIN_STAGING, BRAIN_TESTING};
use crate::{general, utils::block_on}; use crate::{general, utils::block_on};
use ed25519_dalek::SigningKey; use ed25519_dalek::SigningKey;
use log::{debug, info, warn}; use log::{debug, info, warn};
@ -309,20 +310,30 @@ impl Config {
pub fn get_brain_info() -> (String, String) { pub fn get_brain_info() -> (String, String) {
match Self::init_config().network.as_str() { match Self::init_config().network.as_str() {
"staging" => ("https://184.107.169.199:49092".to_string(), "staging-brain".to_string()),
"localhost" => ("https://localhost:31337".to_string(), "staging-brain".to_string()), "localhost" => ("https://localhost:31337".to_string(), "staging-brain".to_string()),
_ => ("https://173.234.17.2:39477".to_string(), "testnet-brain".to_string()), "staging" => {
let url = BRAIN_STAGING.to_string();
log::info!("Using staging brain URL: {url}");
(url, "staging-brain".to_string())
}
_ => {
let url = BRAIN_TESTING.to_string();
log::info!("Using testnet brain URL: {url}");
(url, "testnet-brain".to_string())
}
} }
} }
pub async fn get_brain_channel() -> Result<tonic::transport::Channel, Error> { pub async fn connect_brain_channel(
let (brain_url, brain_san) = Self::get_brain_info(); brain_url: String,
) -> Result<tonic::transport::Channel, Error> {
use hyper_rustls::HttpsConnectorBuilder; use hyper_rustls::HttpsConnectorBuilder;
use rustls::pki_types::pem::PemObject; use rustls::pki_types::pem::PemObject;
use rustls::pki_types::CertificateDer; use rustls::pki_types::CertificateDer;
use rustls::{ClientConfig, RootCertStore}; use rustls::{ClientConfig, RootCertStore};
let brain_san = Config::get_brain_info().1;
let mut detee_root_ca_store = RootCertStore::empty(); let mut detee_root_ca_store = RootCertStore::empty();
detee_root_ca_store detee_root_ca_store
.add(CertificateDer::from_pem_file(Config::get_root_ca_path()?).map_err(|e| { .add(CertificateDer::from_pem_file(Config::get_root_ca_path()?).map_err(|e| {

@ -1 +1,22 @@
use rand::Rng;
use std::sync::LazyLock;
pub const HRATLS_APP_PORT: u32 = 34500; pub const HRATLS_APP_PORT: u32 = 34500;
pub const MAX_REDIRECTS: u16 = 3;
pub const STAGING_BRAIN_URLS: [&str; 3] = [
"https://156.146.63.216:31337", // staging brain 1
"https://156.146.63.216:31337", // staging brain 2
"https://156.146.63.216:31337", // staging brain 3
];
pub const TESTNET_BRAIN_URLS: [&str; 3] = [
"https://184.107.169.199:45223", // testnet brain 1
"https://149.22.95.1:44522", // testnet brain 2
"https://149.36.48.99:48638", // testnet brain 3
];
pub static BRAIN_STAGING: LazyLock<&str> =
LazyLock::new(|| STAGING_BRAIN_URLS[rand::thread_rng().gen_range(0..STAGING_BRAIN_URLS.len())]);
pub static BRAIN_TESTING: LazyLock<&str> =
LazyLock::new(|| TESTNET_BRAIN_URLS[rand::thread_rng().gen_range(0..TESTNET_BRAIN_URLS.len())]);

@ -35,7 +35,8 @@ pub enum Error {
} }
async fn client() -> Result<BrainGeneralCliClient<Channel>, Error> { async fn client() -> Result<BrainGeneralCliClient<Channel>, Error> {
Ok(BrainGeneralCliClient::new(Config::get_brain_channel().await?)) let default_brain_url = Config::get_brain_info().0;
Ok(BrainGeneralCliClient::new(Config::connect_brain_channel(default_brain_url).await?))
} }
pub async fn get_balance(account: &str) -> Result<AccountBalance, Error> { pub async fn get_balance(account: &str) -> Result<AccountBalance, Error> {

@ -80,9 +80,9 @@ fn handle_deploy(
// TODO: maybe add launch config on deploy command with --launch-config flag // TODO: maybe add launch config on deploy command with --launch-config flag
(AppDeployConfig::from_path(file_path).unwrap(), None) (AppDeployConfig::from_path(file_path).unwrap(), None)
} else { } else {
let vcpu = *deploy_match.get_one::<u32>("vcpus").unwrap(); let vcpus = *deploy_match.get_one::<u32>("vcpus").unwrap();
let memory_mb = *deploy_match.get_one::<u32>("memory").unwrap(); let memory_mb = *deploy_match.get_one::<u32>("memory").unwrap();
let disk_mb = *deploy_match.get_one::<u32>("disk").unwrap(); let disk_size_gb = *deploy_match.get_one::<u32>("disk").unwrap();
let port = let port =
deploy_match.get_many::<u32>("port").unwrap_or_default().cloned().collect::<Vec<_>>(); deploy_match.get_many::<u32>("port").unwrap_or_default().cloned().collect::<Vec<_>>();
let package_name = deploy_match.get_one::<String>("package").unwrap().clone(); let package_name = deploy_match.get_one::<String>("package").unwrap().clone();
@ -98,7 +98,7 @@ fn handle_deploy(
let private_package = false; let private_package = false;
let resource = Resource { vcpu, memory_mb, disk_mb, port }; let resource = Resource { vcpus, memory_mb, disk_size_gb, port };
let node_pubkey = match block_on(get_app_node(resource.clone(), location.into())) { let node_pubkey = match block_on(get_app_node(resource.clone(), location.into())) {
Ok(node) => node.node_pubkey, Ok(node) => node.node_pubkey,
Err(e) => { Err(e) => {

@ -7,6 +7,7 @@ use detee_shared::sgx::types::brain::AppDeployConfig;
use tokio_stream::StreamExt; use tokio_stream::StreamExt;
use tonic::transport::Channel; use tonic::transport::Channel;
use crate::call_with_follow_redirect;
use crate::config::Config; use crate::config::Config;
use crate::sgx::utils::calculate_nanolp_for_app; use crate::sgx::utils::calculate_nanolp_for_app;
use crate::utils::{self, sign_request}; use crate::utils::{self, sign_request};
@ -25,6 +26,10 @@ pub enum Error {
CorruptedRootCa(#[from] std::io::Error), CorruptedRootCa(#[from] std::io::Error),
#[error("Internal app error: could not parse Brain URL")] #[error("Internal app error: could not parse Brain URL")]
CorruptedBrainUrl, CorruptedBrainUrl,
#[error("Max redirects exceeded: {0}")]
MaxRedirectsExceeded(String),
#[error("Redirect error: {0}")]
RedirectError(String),
} }
type Result<T> = std::result::Result<T, Error>; type Result<T> = std::result::Result<T, Error>;
@ -48,7 +53,7 @@ impl crate::HumanOutput for AppContract {
.mapped_ports .mapped_ports
.clone() .clone()
.iter() .iter()
.map(|p| format!("({},{})", p.host_port, p.app_port)) .map(|p| format!("({},{})", p.host_port, p.guest_port))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(", "); .join(", ");
println!( println!(
@ -57,8 +62,8 @@ impl crate::HumanOutput for AppContract {
); );
println!("The app has mapped ports by the node are: {mapped_ports}"); println!("The app has mapped ports by the node are: {mapped_ports}");
println!( println!(
"The App has {} vCPUS, {}MB of memory and a disk of {} MB.", "The App has {} vCPUS, {}MB of memory and a disk of {} GB.",
app_resource.vcpu, app_resource.memory_mb, app_resource.disk_mb app_resource.vcpus, app_resource.memory_mb, app_resource.disk_size_gb
); );
println!("You have locked {} nanoLP in the contract, that get collected at a rate of {} nanoLP per minute.", println!("You have locked {} nanoLP in the contract, that get collected at a rate of {} nanoLP per minute.",
self.locked_nano, self.nano_per_minute); self.locked_nano, self.nano_per_minute);
@ -66,7 +71,12 @@ impl crate::HumanOutput for AppContract {
} }
async fn client() -> Result<BrainAppCliClient<Channel>> { async fn client() -> Result<BrainAppCliClient<Channel>> {
Ok(BrainAppCliClient::new(Config::get_brain_channel().await?)) let default_brain_url = Config::get_brain_info().0;
Ok(BrainAppCliClient::new(Config::connect_brain_channel(default_brain_url).await?))
}
async fn client_from_endpoint(reconnect_endpoint: String) -> Result<BrainAppCliClient<Channel>> {
Ok(BrainAppCliClient::new(Config::connect_brain_channel(reconnect_endpoint).await?))
} }
pub async fn new_app(app_deploy_config: AppDeployConfig) -> Result<NewAppRes> { pub async fn new_app(app_deploy_config: AppDeployConfig) -> Result<NewAppRes> {
@ -74,9 +84,9 @@ pub async fn new_app(app_deploy_config: AppDeployConfig) -> Result<NewAppRes> {
let mut req: NewAppReq = app_deploy_config.clone().into(); let mut req: NewAppReq = app_deploy_config.clone().into();
let locked_nano = calculate_nanolp_for_app( let locked_nano = calculate_nanolp_for_app(
resource.vcpu, resource.vcpus,
resource.memory_mb, resource.memory_mb,
resource.disk_mb, resource.disk_size_gb,
app_deploy_config.hours, app_deploy_config.hours,
req.price_per_unit, req.price_per_unit,
); );
@ -86,14 +96,21 @@ pub async fn new_app(app_deploy_config: AppDeployConfig) -> Result<NewAppRes> {
req.admin_pubkey = Config::get_detee_wallet()?; req.admin_pubkey = Config::get_detee_wallet()?;
req.hratls_pubkey = Config::get_hratls_pubkey_hex()?; req.hratls_pubkey = Config::get_hratls_pubkey_hex()?;
let res = client().await?.deploy_app(sign_request(req)?).await?; let client = client().await?;
Ok(res.into_inner()) match call_with_follow_redirect!(client, req, new_app).await {
Ok(res) => Ok(res.into_inner()),
Err(e) => {
log::error!("Failed to create new app: {}", e);
Err(e.into())
}
}
} }
pub async fn delete_app(app_uuid: String) -> Result<()> { pub async fn delete_app(app_uuid: String) -> Result<()> {
let admin_pubkey = Config::get_detee_wallet()?; let admin_pubkey = Config::get_detee_wallet()?;
let delete_req = DelAppReq { uuid: app_uuid, admin_pubkey }; let delete_req = DelAppReq { uuid: app_uuid, admin_pubkey };
let _ = client().await?.delete_app(sign_request(delete_req)?).await?; let client = client().await?;
let _ = call_with_follow_redirect!(client, delete_req, delete_app).await?;
Ok(()) Ok(())
} }

@ -46,7 +46,7 @@ pub async fn connect_app_dtpm_client(app_uuid: &str) -> Result<DtpmConfigManager
let private_key_pem = Config::get_hratls_private_key()?; let private_key_pem = Config::get_hratls_private_key()?;
let (hratls_uri, package_mr_enclave) = hratls_url_and_mr_enclave_from_app_id(app_uuid).await?; let (hratls_uri, package_mr_enclave) = hratls_url_and_mr_enclave_from_app_id(app_uuid).await?;
log::info!("hratls uri: {}\nmr_enclave: {:?}", &hratls_uri, &package_mr_enclave); log::info!("hratls uri: {} mr_enclave: {:?}", &hratls_uri, &package_mr_enclave);
let hratls_config = let hratls_config =
Arc::new(RwLock::new(HRaTlsConfig::new().with_hratls_private_key_pem(private_key_pem))); Arc::new(RwLock::new(HRaTlsConfig::new().with_hratls_private_key_pem(private_key_pem)));

@ -41,11 +41,11 @@ pub struct AppContract {
pub uuid: String, pub uuid: String,
pub name: String, pub name: String,
#[tabled(rename = "Cores")] #[tabled(rename = "Cores")]
pub vcpu: u32, pub vcpus: u32,
#[tabled(rename = "Mem (MB)")] #[tabled(rename = "Mem (MB)")]
pub memory_mb: u32, pub memory_mb: u32,
#[tabled(rename = "Disk (MB)")] #[tabled(rename = "Disk (GB)")]
pub disk_mb: u32, pub disk_size_gb: u32,
#[tabled(rename = "LP/h")] #[tabled(rename = "LP/h")]
pub cost_h: String, pub cost_h: String,
#[tabled(rename = "time left", display_with = "display_mins")] #[tabled(rename = "time left", display_with = "display_mins")]
@ -137,22 +137,22 @@ impl From<AppContractPB> for AppContract {
} }
}; };
let AppResource { vcpu, memory_mb, disk_mb, .. } = let AppResource { vcpus, memory_mb, disk_size_gb, .. } =
brain_app_contract.resource.unwrap_or_default(); brain_app_contract.resource.unwrap_or_default();
let exposed_host_ports = brain_app_contract let exposed_host_ports = brain_app_contract
.mapped_ports .mapped_ports
.iter() .iter()
.map(|port| (port.host_port, port.app_port)) .map(|port| (port.host_port, port.guest_port))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
Self { Self {
location, location,
uuid: brain_app_contract.uuid, uuid: brain_app_contract.uuid,
name: brain_app_contract.app_name, name: brain_app_contract.app_name,
vcpu, vcpus,
memory_mb, memory_mb,
disk_mb, disk_size_gb,
cost_h: format!( cost_h: format!(
"{:.4}", "{:.4}",
(brain_app_contract.nano_per_minute * 60) as f64 / 1_000_000_000.0 (brain_app_contract.nano_per_minute * 60) as f64 / 1_000_000_000.0
@ -181,7 +181,6 @@ pub async fn get_one_contract(uuid: &str) -> Result<AppContractPB, Error> {
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct AppDeployResponse { pub struct AppDeployResponse {
pub status: String,
pub uuid: String, pub uuid: String,
pub name: String, pub name: String,
pub node_ip: String, pub node_ip: String,
@ -198,14 +197,13 @@ impl crate::HumanOutput for AppDeployResponse {
impl From<(NewAppRes, String)> for AppDeployResponse { impl From<(NewAppRes, String)> for AppDeployResponse {
fn from((value, name): (NewAppRes, String)) -> Self { fn from((value, name): (NewAppRes, String)) -> Self {
Self { Self {
status: value.status,
uuid: value.uuid, uuid: value.uuid,
name, name,
node_ip: value.ip_address, node_ip: value.ip_address,
hratls_port: value hratls_port: value
.mapped_ports .mapped_ports
.iter() .iter()
.find(|port| port.app_port == HRATLS_APP_PORT) .find(|port| port.guest_port == HRATLS_APP_PORT)
.map(|port| port.host_port) .map(|port| port.host_port)
.unwrap_or(HRATLS_APP_PORT), .unwrap_or(HRATLS_APP_PORT),
error: value.error, error: value.error,
@ -230,14 +228,15 @@ pub async fn get_app_node(
location: snp::deploy::Location, location: snp::deploy::Location,
) -> Result<AppNodeListResp, grpc_brain::Error> { ) -> Result<AppNodeListResp, grpc_brain::Error> {
let app_node_filter = AppNodeFilters { let app_node_filter = AppNodeFilters {
vcpus: resource.vcpu, vcpus: resource.vcpus,
memory_mb: resource.memory_mb, memory_mb: resource.memory_mb,
storage_mb: resource.disk_mb, storage_gb: resource.disk_size_gb,
country: location.country.clone().unwrap_or_default(), country: location.country.clone().unwrap_or_default(),
region: location.region.clone().unwrap_or_default(), region: location.region.clone().unwrap_or_default(),
city: location.city.clone().unwrap_or_default(), city: location.city.clone().unwrap_or_default(),
ip: location.node_ip.clone().unwrap_or_default(), ip: location.node_ip.clone().unwrap_or_default(),
node_pubkey: String::new(), node_pubkey: String::new(),
free_ports: (resource.port.len() + 1) as u32,
}; };
get_one_app_node(app_node_filter).await get_one_app_node(app_node_filter).await
} }

@ -48,7 +48,7 @@ pub async fn hratls_url_and_mr_enclave_from_app_id(
let dtpm_port = app_contract let dtpm_port = app_contract
.mapped_ports .mapped_ports
.iter() .iter()
.find(|port| port.app_port == HRATLS_APP_PORT) .find(|port| port.guest_port == HRATLS_APP_PORT)
.ok_or(crate::sgx::grpc_dtpm::Error::Dtpm("Could not find DTMP port".to_string()))? .ok_or(crate::sgx::grpc_dtpm::Error::Dtpm("Could not find DTMP port".to_string()))?
.host_port; .host_port;
@ -71,13 +71,13 @@ pub async fn fetch_config(package_name: &str) -> Result<DtpmConfig, Error> {
pub fn calculate_nanolp_for_app( pub fn calculate_nanolp_for_app(
vcpus: u32, vcpus: u32,
memory_mb: u32, memory_mb: u32,
disk_size_mb: u32, disk_size_gb: u32,
hours: u64, hours: u64,
node_price: u64, node_price: u64,
) -> u64 { ) -> u64 {
// this calculation needs to match the calculation of the network // this calculation needs to match the calculation of the network
let total_units = let total_units =
(vcpus as f64 * 5f64) + (memory_mb as f64 / 200f64) + (disk_size_mb as f64 / 10000f64); (vcpus as f64 * 5f64) + (memory_mb as f64 / 200f64) + (disk_size_gb as f64 / 10f64);
let locked_nano = (hours as f64 * 60f64 * total_units * node_price as f64) as u64; let locked_nano = (hours as f64 * 60f64 * total_units * node_price as f64) as u64;
eprintln!( eprintln!(
"Node price: {}/unit/minute. Total Units for hardware requested: {:.4}. Locking {} LP (offering the App for {} hours).", "Node price: {}/unit/minute. Total Units for hardware requested: {:.4}. Locking {} LP (offering the App for {} hours).",

@ -3,7 +3,9 @@ pub mod proto {
pub use detee_shared::vm_proto::*; pub use detee_shared::vm_proto::*;
} }
use crate::call_with_follow_redirect;
use crate::config::Config; use crate::config::Config;
use crate::utils::{self, sign_request};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use log::{debug, info, warn}; use log::{debug, info, warn};
use proto::{ use proto::{
@ -12,9 +14,7 @@ use proto::{
}; };
use tokio_stream::StreamExt; use tokio_stream::StreamExt;
use tonic::metadata::errors::InvalidMetadataValue; use tonic::metadata::errors::InvalidMetadataValue;
use tonic::metadata::AsciiMetadataValue;
use tonic::transport::Channel; use tonic::transport::Channel;
use tonic::Request;
lazy_static! { lazy_static! {
static ref SECURE_PUBLIC_KEY: String = use_default_string(); static ref SECURE_PUBLIC_KEY: String = use_default_string();
@ -41,6 +41,12 @@ pub enum Error {
CorruptedRootCa(#[from] std::io::Error), CorruptedRootCa(#[from] std::io::Error),
#[error("Internal app error: could not parse Brain URL")] #[error("Internal app error: could not parse Brain URL")]
CorruptedBrainUrl, CorruptedBrainUrl,
#[error("Max redirects exceeded: {0}")]
MaxRedirectsExceeded(String),
#[error("Redirect error: {0}")]
RedirectError(String),
#[error(transparent)]
InternalError(#[from] utils::Error),
} }
impl crate::HumanOutput for VmContract { impl crate::HumanOutput for VmContract {
@ -84,21 +90,15 @@ impl crate::HumanOutput for VmNodeListResp {
} }
async fn client() -> Result<BrainVmCliClient<Channel>, Error> { async fn client() -> Result<BrainVmCliClient<Channel>, Error> {
Ok(BrainVmCliClient::new(Config::get_brain_channel().await?)) let default_brain_url = Config::get_brain_info().0;
info!("brain_url: {default_brain_url}");
Ok(BrainVmCliClient::new(Config::connect_brain_channel(default_brain_url).await?))
} }
fn sign_request<T: std::fmt::Debug>(req: T) -> Result<Request<T>, Error> { async fn client_from_endpoint(
let pubkey = Config::get_detee_wallet()?; reconnect_endpoint: String,
let timestamp = chrono::Utc::now().to_rfc3339(); ) -> Result<BrainVmCliClient<Channel>, Error> {
let signature = Config::try_sign_message(&format!("{timestamp}{req:?}"))?; Ok(BrainVmCliClient::new(Config::connect_brain_channel(reconnect_endpoint).await?))
let timestamp: AsciiMetadataValue = timestamp.parse()?;
let pubkey: AsciiMetadataValue = pubkey.parse()?;
let signature: AsciiMetadataValue = signature.parse()?;
let mut req = Request::new(req);
req.metadata_mut().insert("timestamp", timestamp);
req.metadata_mut().insert("pubkey", pubkey);
req.metadata_mut().insert("request-signature", signature);
Ok(req)
} }
pub async fn get_node_list(req: VmNodeFilters) -> Result<Vec<VmNodeListResp>, Error> { pub async fn get_node_list(req: VmNodeFilters) -> Result<Vec<VmNodeListResp>, Error> {
@ -128,9 +128,10 @@ pub async fn get_one_node(req: VmNodeFilters) -> Result<VmNodeListResp, Error> {
} }
pub async fn create_vm(req: NewVmReq) -> Result<NewVmResp, Error> { pub async fn create_vm(req: NewVmReq) -> Result<NewVmResp, Error> {
let mut client = client().await?;
debug!("Sending NewVmReq to brain: {req:?}"); debug!("Sending NewVmReq to brain: {req:?}");
match client.new_vm(sign_request(req)?).await {
let client = client().await?;
match call_with_follow_redirect!(client, req, new_vm).await {
Ok(resp) => Ok(resp.into_inner()), Ok(resp) => Ok(resp.into_inner()),
Err(e) => Err(e.into()), Err(e) => Err(e.into()),
} }
@ -157,10 +158,9 @@ pub async fn list_contracts(req: ListVmContractsReq) -> Result<Vec<VmContract>,
} }
pub async fn delete_vm(uuid: &str) -> Result<(), Error> { pub async fn delete_vm(uuid: &str) -> Result<(), Error> {
let mut client = client().await?; let client = client().await?;
let req = DeleteVmReq { uuid: uuid.to_string(), admin_pubkey: Config::get_detee_wallet()? }; let req = DeleteVmReq { uuid: uuid.to_string(), admin_pubkey: Config::get_detee_wallet()? };
let result = client.delete_vm(sign_request(req)?).await; match call_with_follow_redirect!(client, req, delete_vm).await {
match result {
Ok(confirmation) => { Ok(confirmation) => {
log::debug!("VM deletion confirmation: {confirmation:?}"); log::debug!("VM deletion confirmation: {confirmation:?}");
} }
@ -195,9 +195,8 @@ pub async fn extend_vm(uuid: String, admin_pubkey: String, locked_nano: u64) ->
pub async fn update_vm(req: UpdateVmReq) -> Result<UpdateVmResp, Error> { pub async fn update_vm(req: UpdateVmReq) -> Result<UpdateVmResp, Error> {
info!("Updating VM {req:?}"); info!("Updating VM {req:?}");
let mut client = client().await?; let client = client().await?;
let result = client.update_vm(sign_request(req)?).await; match call_with_follow_redirect!(client, req, update_vm).await {
match result {
Ok(resp) => { Ok(resp) => {
let resp = resp.into_inner(); let resp = resp.into_inner();
if resp.error.is_empty() { if resp.error.is_empty() {

@ -42,3 +42,53 @@ pub fn shorten_string(my_string: &String) -> String {
format!("{}", first_part) format!("{}", first_part)
} }
} }
#[macro_export]
macro_rules! call_with_follow_redirect {
(
$client:expr,
$req_data:expr,
$method:ident
) => {
async {
let mut client = $client;
for attempt in 0..crate::constants::MAX_REDIRECTS {
log::debug!(
"Attempt #{}: Calling method '{}'...",
attempt + 1,
stringify!($method)
);
let req_data_clone = $req_data.clone();
let signed_req = crate::utils::sign_request(req_data_clone)?;
match client.$method(signed_req).await {
Ok(resp) => return Ok(resp),
Err(status)
if status.code() == tonic::Code::Unavailable
&& status.message() == "moved" =>
{
let redirect_url = status
.metadata()
.get("location")
.and_then(|v| v.to_str().ok())
.ok_or_else(|| {
Error::RedirectError(
"Server indicated a move but provided no location".into(),
)
})?;
log::info!("Server moved. Redirecting to {}...", redirect_url);
client = client_from_endpoint(format!("https://{}", redirect_url)).await?;
continue;
}
Err(e) => return Err(Error::ResponseStatus(e)),
}
}
Err(Error::MaxRedirectsExceeded(crate::constants::MAX_REDIRECTS.to_string()))
}
};
}