Redirect to pubsub node and some bug fixes #8

Merged
ghe0 merged 7 commits from pubsub_redirect into surreal_brain 2025-06-19 17:39:56 +00:00
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()))
}
};
}