diff --git a/Cargo.lock b/Cargo.lock index abd2e50..4a44855 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,7 +1,6 @@ -# SPDX-License-Identifier: Apache-2.0 - # This file is automatically @generated by Cargo. # It is not intended for manual editing. +# SPDX-License-Identifier: Apache-2.0 version = 4 [[package]] @@ -1184,7 +1183,7 @@ dependencies = [ [[package]] name = "detee-shared" version = "0.1.0" -source = "git+ssh://git@gitea.detee.cloud/testnet/proto.git?branch=surreal_brain_app#0b195b4589e4ec689af7ddca27dc051716ecee78" +source = "git+ssh://git@gitea.detee.cloud/testnet/proto.git?branch=credits-v2#6d377926408953e8da2c0f4c6625d4fb90ba7652" dependencies = [ "bincode", "prost", diff --git a/Cargo.toml b/Cargo.toml index 757ae85..824da56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ tokio-retry = "0.3.0" 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"] } -detee-shared = { git = "ssh://git@gitea.detee.cloud/testnet/proto.git", branch = "surreal_brain_app" } +detee-shared = { git = "ssh://git@gitea.detee.cloud/testnet/proto.git", branch = "credits-v2" } # detee-shared = { path = "../detee-shared" } [build-dependencies] diff --git a/samples/new_vm/advanced_os_config.yaml b/samples/new_vm/advanced_os_config.yaml index 39ce689..5bfebba 100644 --- a/samples/new_vm/advanced_os_config.yaml +++ b/samples/new_vm/advanced_os_config.yaml @@ -11,8 +11,8 @@ ipv4: !PublishPorts # ipv4: !PublishPorts [ 80, 8080 ] public_ipv6: false vcpus: 2 -memory_mb: 2000 -disk_size_gb: 20 +memory_gib: 2000 +disk_size_gib: 20 # os_setup is an optional field that allows you to specify the operating system # dtrfs is the DeTEE initramfs required to boot a VM. It also needs a kernel. # The OS Template is normally a Linux distribution (without initrd and kernel) diff --git a/samples/new_vm/no_public_ips.yaml b/samples/new_vm/no_public_ips.yaml index a83c7da..4459e4f 100644 --- a/samples/new_vm/no_public_ips.yaml +++ b/samples/new_vm/no_public_ips.yaml @@ -11,5 +11,5 @@ ipv4: !PublishPorts # ipv4: !PublishPorts [ 80, 8080 ] public_ipv6: false vcpus: 2 -memory_mb: 2000 -disk_size_gb: 20 +memory_gib: 2 +disk_size_gib: 20 diff --git a/samples/new_vm/public_ipv4_and_ipv6.yaml b/samples/new_vm/public_ipv4_and_ipv6.yaml index d6af1a6..1c8777a 100644 --- a/samples/new_vm/public_ipv4_and_ipv6.yaml +++ b/samples/new_vm/public_ipv4_and_ipv6.yaml @@ -13,5 +13,5 @@ ipv4: !PublicIPv4 # For IPv6, just specify true or false if you want a public IP public_ipv6: true vcpus: 2 -memory_mb: 2000 -disk_size_gb: 20 +memory_gib: 2 +disk_size_gib: 20 diff --git a/samples/new_vm/specific_city.yaml b/samples/new_vm/specific_city.yaml index 8871e1b..3ce14a2 100644 --- a/samples/new_vm/specific_city.yaml +++ b/samples/new_vm/specific_city.yaml @@ -12,5 +12,5 @@ location: ipv4: !PublicIPv4 public_ipv6: false vcpus: 2 -memory_mb: 1000 -disk_size_gb: 20 +memory_gib: 1000 +disk_size_gib: 20 diff --git a/src/bin/detee-cli.rs b/src/bin/detee-cli.rs index 484aedd..6feaac2 100644 --- a/src/bin/detee-cli.rs +++ b/src/bin/detee-cli.rs @@ -1,12 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 use clap::{builder::PossibleValue, Arg, Command}; -use detee_cli::general::cli_handler::{ - handle_account, handle_completion, handle_operators, handle_packagers, +use detee_cli::{ + general::cli_handler::{handle_account, handle_completion, handle_operators, handle_packagers}, + sgx::cli_handler::{handle_app, handle_app_nodes}, + snp::cli_handler::{handle_vm, handle_vm_nodes}, + *, }; -use detee_cli::sgx::cli_handler::{handle_app, handle_app_nodes}; -use detee_cli::snp::cli_handler::{handle_vm, handle_vm_nodes}; -use detee_cli::*; const ABOUT: &str = r#"The DeTEE CLI allows you to manage and deploy applications and virtual machines. All software runs within Trusted Execution Environments on a distributed network. @@ -53,6 +53,16 @@ fn main() { } fn clap_cmd() -> Command { + let snp_locations = [ + PossibleValue::new("GB").help("London, England, GB"), + PossibleValue::new("Canada").help("Montréal or Vancouver"), + PossibleValue::new("Montreal").help("Montréal, Quebec, CA"), + PossibleValue::new("Vancouver").help("Vancouver, British Columbia, CA"), + PossibleValue::new("California").help("San Jose, California, US"), + PossibleValue::new("US").help("San Jose, California, US"), + PossibleValue::new("France").help("Paris, Île-de-France, FR"), + PossibleValue::new("Any").help("List offers for any location."), + ]; Command::new("detee-cli") .version(build::CLAP_LONG_VERSION) .author("https://detee.ltd") @@ -160,7 +170,7 @@ fn clap_cmd() -> Command { .help("for how many hours should the app run") .default_value("1") .value_parser(clap::value_parser!(u64).range(1..5000)) - .long_help("How long should the app run for so it locks up LP accordingly") + .long_help("How long should the app run for so it locks up credits accordingly") ) .arg( Arg::new("price") @@ -325,17 +335,8 @@ fn clap_cmd() -> Command { Arg::new("location") .help("deploy to a specific location") .long("location") - .default_value("Vancouver") - .value_parser([ - PossibleValue::new("GB").help("London, England, GB"), - PossibleValue::new("Canada").help("Montréal or Vancouver"), - PossibleValue::new("Montreal").help("Montréal, Quebec, CA"), - PossibleValue::new("Vancouver").help("Vancouver, British Columbia, CA"), - PossibleValue::new("California").help("San Jose, California, US"), - PossibleValue::new("US").help("San Jose, California, US"), - PossibleValue::new("France").help("Paris, Île-de-France, FR"), - PossibleValue::new("Random").help("Just deploy somewhere..."), - ]), + .default_value("Any") + .value_parser(snp_locations.clone()), ) .arg( Arg::new("vcpus") @@ -347,16 +348,16 @@ fn clap_cmd() -> Command { .arg( Arg::new("memory") .long("memory") - .default_value("1000") - .value_parser(clap::value_parser!(u32).range(800..123000)) - .help("memory in MB") + .default_value("1") + .value_parser(clap::value_parser!(u32).range(1..500)) + .help("memory in GiB") ) .arg( Arg::new("disk") .long("disk") .default_value("10") .value_parser(clap::value_parser!(u32).range(5..500)) - .help("disk size in GB") + .help("disk size in GiB") ) .arg( Arg::new("distribution") @@ -375,8 +376,8 @@ fn clap_cmd() -> Command { .arg( Arg::new("price") .long("price") - .help("price per unit per minute; check docs") - .default_value("20000") + .help("maxium accepted price per unit per minute") + .default_value("4000") .value_parser(clap::value_parser!(u64).range(1..50000000)) ) .arg( @@ -437,7 +438,7 @@ fn clap_cmd() -> Command { .long_about("Allows you to update the hardware or the lifetime".to_string() + "\nAny hardware modifiations will restart the VM." + "\nChanging the lifetime of a VM will not restart." + - "\nIf changing the lifetime to a higher value, LP will locked accordingly.") + "\nIf changing the lifetime to a higher value, credits will locked accordingly.") .arg( Arg::new("uuid") .help("supply the uuid of the VM you wish to upgrade") @@ -460,15 +461,15 @@ fn clap_cmd() -> Command { Arg::new("memory") .long("memory") .default_value("0") - .value_parser(clap::value_parser!(u32).range(0..115000)) - .help("modify the MB of memory reserved") + .value_parser(clap::value_parser!(u32).range(0..5000)) + .help("modify the GiB of memory reserved") ) .arg( Arg::new("disk") .long("disk") .default_value("0") .value_parser(clap::value_parser!(u32).range(0..500)) - .help("increase the size of the disk in GB") + .help("increase the size of the disk in GiB") ) .arg( Arg::new("hours") @@ -501,7 +502,24 @@ fn clap_cmd() -> Command { ) .subcommand(Command::new("vm-node") .about("info about AMD SEV-SNP servers registerd to DeTEE") - .subcommand(Command::new("search").about("search nodes based on filters")) + .subcommand(Command::new("search").about("search nodes based on filters") + .arg( + Arg::new("location") + .help("deploy to a specific location") + .long("location") + .default_value("Any") + .value_parser(snp_locations.clone()), + ) + ) + .subcommand(Command::new("offers").about("search nodes based on filters") + .arg( + Arg::new("location") + .help("deploy to a specific location") + .long("location") + .default_value("Any") + .value_parser(snp_locations), + ) + ) .subcommand(Command::new("inspect").about("get detailed information about a node") .arg( Arg::new("ip") @@ -537,7 +555,7 @@ fn clap_cmd() -> Command { .arg( Arg::new("escrow") .long("escrow") - .help("At least 5000 LP is required as escrow") + .help("At least 5000 credits is required as escrow") .long_help("Escrow is used by node operators to guarantee quality.".to_owned() + "\nBefore adding escrow, make sure you booted a node under your account." + "\nWhen all your nodes got decomissioned, your escrow gets automatically returned.") diff --git a/src/config.rs b/src/config.rs index ab08576..30735ec 100644 --- a/src/config.rs +++ b/src/config.rs @@ -36,10 +36,10 @@ impl super::HumanOutput for AccountData { } if !self.wallet_path.is_empty() { println!("The address of your DeTEE wallet is {}", self.wallet_address); - println!("The balance of your account is {} LP", self.account_balance); + println!("The balance of your account is {} credits", self.account_balance); if self.locked_funds != 0.0 { println!( - "WARNING! {} LP is temporary locked, waiting for a Contract.", + "WARNING! {} credits is temporary locked, waiting for a Contract.", self.locked_funds ); } diff --git a/src/general/grpc.rs b/src/general/grpc.rs index 540db7b..7117337 100644 --- a/src/general/grpc.rs +++ b/src/general/grpc.rs @@ -96,7 +96,7 @@ pub async fn kick_contract(contract_uuid: String, reason: String) -> Result Result<(), Error> { diff --git a/src/general/operators.rs b/src/general/operators.rs index 39c4194..229508a 100644 --- a/src/general/operators.rs +++ b/src/general/operators.rs @@ -35,7 +35,7 @@ pub fn register(escrow: u64, email: String) -> Result Result, grpc::Er pub fn kick(contract_uuid: String, reason: String) -> Result { let nano_lp = block_on(grpc::kick_contract(contract_uuid, reason))?; Ok(crate::SimpleOutput::from( - format!("Successfully terminated contract. Refunded {} nanoLP.", nano_lp).as_str(), + format!("Successfully terminated contract. Refunded {} nanocredits.", nano_lp).as_str(), )) } diff --git a/src/sgx/grpc_brain.rs b/src/sgx/grpc_brain.rs index c507980..bbba26f 100644 --- a/src/sgx/grpc_brain.rs +++ b/src/sgx/grpc_brain.rs @@ -67,7 +67,7 @@ impl crate::HumanOutput for AppContract { "The App has {} vCPUS, {}MB of memory and a disk of {} GB.", 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 {} nanocredits in the contract, that get collected at a rate of {} nanocredits per minute.", self.locked_nano, self.nano_per_minute); } } diff --git a/src/sgx/mod.rs b/src/sgx/mod.rs index 6c1ea0c..d8a0161 100644 --- a/src/sgx/mod.rs +++ b/src/sgx/mod.rs @@ -48,7 +48,7 @@ pub struct AppContract { pub memory_mb: u32, #[tabled(rename = "Disk (GB)")] pub disk_size_gb: u32, - #[tabled(rename = "LP/h")] + #[tabled(rename = "credits/h")] pub cost_h: String, #[tabled(rename = "time left", display_with = "display_mins")] pub time_left: u64, @@ -227,7 +227,7 @@ impl crate::HumanOutput for AppDeleteResponse { pub async fn get_app_node( resource: Resource, - location: snp::deploy::Location, + location: snp::Location, ) -> Result { let app_node_filter = AppNodeFilters { vcpus: resource.vcpus, @@ -268,7 +268,7 @@ impl From for TabledAppNode { operator: brain_node.operator, location: brain_node.city + ", " + &brain_node.region + ", " + &brain_node.country, public_ip: brain_node.ip, - price: format!("{} nanoLP/min", brain_node.price), + price: format!("{} nanocredits/min", brain_node.price), reports: brain_node.reports.len(), } } diff --git a/src/snp/cli_handler.rs b/src/snp/cli_handler.rs index db206ea..129ca42 100644 --- a/src/snp/cli_handler.rs +++ b/src/snp/cli_handler.rs @@ -1,9 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -use crate::general; -use crate::name_generator; -use crate::snp; -use crate::{cli_print, SimpleOutput}; +use crate::{cli_print, general, name_generator, snp, SimpleOutput}; use clap::ArgMatches; use std::error::Error; @@ -30,7 +27,14 @@ pub fn handle_vm(matches: &ArgMatches) { pub fn handle_vm_nodes(matches: &ArgMatches) { match matches.subcommand() { - Some(("search", _)) => cli_print(snp::print_nodes().map_err(Into::into)), + Some(("search", arguments)) => { + let location = arguments.get_one::("location").unwrap().as_str(); + cli_print(snp::search_nodes(location.into()).map_err(Into::into)); + } + Some(("offers", arguments)) => { + let location = arguments.get_one::("location").unwrap().as_str(); + cli_print(snp::print_node_offers(location.into()).map_err(Into::into)); + } Some(("inspect", path_subcommand)) => { let ip: String = path_subcommand.get_one::("ip").unwrap().clone(); cli_print(snp::inspect_node(ip).map_err(Into::into)); @@ -69,8 +73,8 @@ fn handle_vm_deploy(matches: &ArgMatches) -> Result("vcpus").unwrap(), - memory_mb: *matches.get_one::("memory").unwrap(), - disk_size_gb: *matches.get_one::("disk").unwrap(), + memory_gib: *matches.get_one::("memory").unwrap(), + disk_size_gib: *matches.get_one::("disk").unwrap(), dtrfs: None, hours: *matches.get_one::("hours").unwrap(), price: *matches.get_one::("price").unwrap(), @@ -92,10 +96,6 @@ fn handle_vm_update(update_vm_args: &ArgMatches) -> Result("uuid").unwrap().clone(); let hostname = update_vm_args.get_one::("hostname").unwrap().clone(); let memory = *update_vm_args.get_one::("memory").unwrap(); - if memory > 0 && memory < 800 { - log::error!("At least 800MB of memory must be assgined to the VM"); - return Ok(SimpleOutput::from("")); - } snp::update::Request::process_request( hostname, &uuid, diff --git a/src/snp/deploy.rs b/src/snp/deploy.rs index 8d11d29..d802eee 100644 --- a/src/snp/deploy.rs +++ b/src/snp/deploy.rs @@ -4,8 +4,7 @@ use super::{ grpc::{self, proto}, injector, Distro, Dtrfs, Error, VmSshArgs, DEFAULT_ARCHLINUX, DEFAULT_DTRFS, }; -use crate::config::Config; -use crate::utils::block_on; +use crate::{config::Config, utils::block_on}; use log::{debug, info}; use serde::{Deserialize, Serialize}; @@ -15,44 +14,18 @@ pub enum IPv4Config { PublicIPv4, } -// TODO: push this out of snp module -#[derive(Serialize, Deserialize, Default)] -pub struct Location { - pub node_ip: Option, - pub country: Option, - pub region: Option, - pub city: Option, -} - -impl From<&str> for Location { - fn from(s: &str) -> Self { - match s { - "Canada" => Self { country: Some("CA".to_string()), ..Default::default() }, - "Montreal" => Self { city: Some("Montréal".to_string()), ..Default::default() }, - "Vancouver" => Self { city: Some("Vancouver".to_string()), ..Default::default() }, - "US" => Self { country: Some("US".to_string()), ..Default::default() }, - "California" => Self { country: Some("US".to_string()), ..Default::default() }, - "France" => Self { country: Some("FR".to_string()), ..Default::default() }, - "GB" => Self { country: Some("GB".to_string()), ..Default::default() }, - "Random" => Self { ..Default::default() }, - "DE" => Self { country: Some("DE".to_string()), ..Default::default() }, - _ => Self { city: Some("Vancouver".to_string()), ..Default::default() }, - } - } -} - #[derive(Serialize, Deserialize)] pub struct Request { pub hostname: String, pub hours: u32, // price per unit per minute pub price: u64, - pub location: Location, + pub location: super::Location, pub ipv4: IPv4Config, pub public_ipv6: bool, pub vcpus: u32, - pub memory_mb: u32, - pub disk_size_gb: u32, + pub memory_gib: u32, + pub disk_size_gib: u32, pub dtrfs: Option, pub distro: Option, } @@ -70,8 +43,8 @@ impl Request { } pub fn deploy(&self) -> Result { - let (node_ip, new_vm_resp) = self.send_vm_request()?; - info!("Got confirmation from the node {node_ip} that VM started."); + let (vcpus, new_vm_resp) = self.calculate_and_send_request()?; + info!("Got confirmation from the node that the VM started."); debug!("IPs and ports assigned by node are: {new_vm_resp:#?}"); if !new_vm_resp.error.is_empty() { return Err(Error::Node(new_vm_resp.error)); @@ -83,7 +56,7 @@ impl Request { let args = new_vm_resp.args.ok_or(Error::NoMeasurement)?; let measurement_args = injector::Args { uuid: new_vm_resp.uuid.clone(), - vcpus: self.vcpus, + vcpus, kernel: kernel_sha, initrd: dtrfs_sha, args: args.clone(), @@ -107,10 +80,106 @@ impl Request { Ok(ssh_args) } - // returns node IP and data regarding the new VM - fn send_vm_request(&self) -> Result<(String, proto::NewVmResp), Error> { - let admin_pubkey = Config::get_detee_wallet()?; - let node = self.get_node()?; + /// returns number of vCPUs and response from the daemon + fn calculate_and_send_request(&self) -> Result<(u32, proto::NewVmResp), Error> { + let new_vm_req = self.get_cheapest_offer()?; + let vcpus = new_vm_req.vcpus; + + eprintln!( + "Locking {} credits for {} hours of the following HW spec: {} vCPUs, {} MiB Mem, {} MiB Disk", + new_vm_req.locked_nano as f64 / 1_000_000_000_f64, + self.hours, + new_vm_req.vcpus, + new_vm_req.memory_mib, + new_vm_req.disk_size_mib + ); + + // eprint!( + // "Node price: {}/unit/minute. Total Units for hardware requested: {}. ", + // node_price as f64 / 1_000_000_000.0, + // total_units, + // ); + // eprintln!( + // "Locking {} LP (offering the VM for {} hours).", + // locked_nano as f64 / 1_000_000_000.0, + // hours + // ); + + let new_vm_resp = block_on(grpc::create_vm(new_vm_req))?; + if !new_vm_resp.error.is_empty() { + return Err(Error::Node(new_vm_resp.error)); + } + Ok((vcpus, new_vm_resp)) + } + + fn get_cheapest_offer(&self) -> Result { + let (free_ports, offers_ipv4) = match &self.ipv4 { + IPv4Config::PublishPorts(vec) => (vec.len() as u32, false), + IPv4Config::PublicIPv4 => (0, true), + }; + let filters = proto::VmNodeFilters { + free_ports, + offers_ipv4, + offers_ipv6: self.public_ipv6, + vcpus: self.vcpus, + memory_mib: self.memory_gib * 1024, + storage_mib: self.disk_size_gib * 1024, + country: self.location.country.clone().unwrap_or_default(), + region: self.location.region.clone().unwrap_or_default(), + city: self.location.city.clone().unwrap_or_default(), + ip: self.location.node_ip.clone().unwrap_or_default(), + node_pubkey: String::new(), + }; + let node_list = match block_on(grpc::get_node_list(filters)) { + Ok(node_list) => Ok(node_list), + Err(e) => { + log::error!("Coult not get node from brain: {e:?}"); + Err(Error::NoValidNodeFound) + } + }?; + + let mut node_list_iter = node_list.iter(); + let mut final_request = self.calculate_vm_request( + Config::get_detee_wallet()?, + node_list_iter.next().ok_or(Error::NoValidNodeFound)?, + ); + while let Some(node) = node_list_iter.next() { + let new_vm_req = self.calculate_vm_request(Config::get_detee_wallet()?, node); + if new_vm_req.locked_nano < final_request.locked_nano { + final_request = new_vm_req; + } + } + + Ok(final_request) + } + + fn calculate_vm_request( + &self, + admin_pubkey: String, + node: &proto::VmNodeListResp, + ) -> proto::NewVmReq { + let memory_per_cpu = node.memory_mib / node.vcpus; + let disk_per_cpu = node.disk_mib / node.vcpus; + let mut vcpus = self.vcpus; + if vcpus < (self.memory_gib * 1024).div_ceil(memory_per_cpu as u32) { + vcpus = (self.memory_gib * 1024).div_ceil(memory_per_cpu as u32); + } + if vcpus < (self.disk_size_gib * 1024).div_ceil(disk_per_cpu as u32) { + vcpus = (self.disk_size_gib * 1024).div_ceil(disk_per_cpu as u32); + } + + let memory_mib = vcpus * memory_per_cpu as u32; + let disk_size_mib = vcpus * disk_per_cpu as u32; + + let nanocredits = super::calculate_nanocredits( + vcpus, + memory_mib, + disk_size_mib, + node.public_ipv4, + self.hours, + node.price, + ); + let (extra_ports, public_ipv4): (Vec, bool) = match &self.ipv4 { IPv4Config::PublishPorts(vec) => (vec.to_vec(), false), IPv4Config::PublicIPv4 => (Vec::new(), true), @@ -124,63 +193,31 @@ impl Request { DEFAULT_DTRFS.dtrfs_sha.clone(), ), }; - let locked_nano = super::calculate_nanolp( - self.vcpus, - self.memory_mb, - self.disk_size_gb, - public_ipv4, - self.hours, - self.price, - ); + let brain_req = proto::NewVmReq { uuid: String::new(), hostname: self.hostname.clone(), admin_pubkey, - node_pubkey: node.node_pubkey, + node_pubkey: node.node_pubkey.clone(), extra_ports, public_ipv4, public_ipv6: self.public_ipv6, - disk_size_gb: self.disk_size_gb, - vcpus: self.vcpus, - memory_mb: self.memory_mb, + disk_size_mib, + vcpus, + memory_mib, kernel_url, kernel_sha, dtrfs_url, dtrfs_sha, - price_per_unit: self.price, - locked_nano, + price_per_unit: node.price, + locked_nano: nanocredits, }; - let new_vm_resp = block_on(grpc::create_vm(brain_req))?; - if !new_vm_resp.error.is_empty() { - return Err(Error::Node(new_vm_resp.error)); - } - Ok((node.ip, new_vm_resp)) - } - pub fn get_node(&self) -> Result { - let (free_ports, offers_ipv4) = match &self.ipv4 { - IPv4Config::PublishPorts(vec) => (vec.len() as u32, false), - IPv4Config::PublicIPv4 => (0, true), - }; - let filters = proto::VmNodeFilters { - free_ports, - offers_ipv4, - offers_ipv6: self.public_ipv6, - vcpus: self.vcpus, - memory_mb: self.memory_mb, - storage_gb: self.disk_size_gb, - country: self.location.country.clone().unwrap_or_default(), - region: self.location.region.clone().unwrap_or_default(), - city: self.location.city.clone().unwrap_or_default(), - ip: self.location.node_ip.clone().unwrap_or_default(), - node_pubkey: String::new(), - }; - match block_on(grpc::get_one_node(filters)) { - Ok(node) => Ok(node), - Err(e) => { - log::error!("Coult not get node from brain: {e:?}"); - Err(Error::NoValidNodeFound) - } - } + debug!( + "Node {} can offer the VM at {} nanocredits for {} hours. Spec: {} vCPUs, {} MiB mem, {} MiB disk.", + node.ip, brain_req.locked_nano, self.hours, brain_req.vcpus, brain_req.memory_mib, brain_req.disk_size_mib + ); + + brain_req } } diff --git a/src/snp/grpc.rs b/src/snp/grpc.rs index 45f0549..cade76f 100644 --- a/src/snp/grpc.rs +++ b/src/snp/grpc.rs @@ -74,7 +74,7 @@ impl crate::HumanOutput for VmContract { "The VM has {} vCPUS, {}MB of memory and a disk of {} GB.", self.vcpus, self.memory_mb, self.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 {} nanocredits in the contract, that get collected at a rate of {} nanocredits per minute.", self.locked_nano, self.nano_per_minute); } } @@ -182,7 +182,7 @@ pub async fn extend_vm(uuid: String, admin_pubkey: String, locked_nano: u64) -> Ok(confirmation) => { log::debug!("VM contract extension confirmation: {confirmation:?}"); log::info!( - "VM contract got updated. It now has {} LP locked for the VM.", + "VM contract got updated. It now has {} credits locked for the VM.", locked_nano as f64 / 1_000_000_000.0 ); } diff --git a/src/snp/mod.rs b/src/snp/mod.rs index 3de89ba..80682fd 100644 --- a/src/snp/mod.rs +++ b/src/snp/mod.rs @@ -6,11 +6,10 @@ pub mod grpc; mod injector; pub mod update; -use crate::utils::block_on; -use crate::utils::shorten_string; use crate::{ config::{self, Config}, snp, + utils::{block_on, display_mib_or_gib, shorten_string}, }; use grpc::proto; use lazy_static::lazy_static; @@ -39,6 +38,32 @@ pub enum Error { Injector(#[from] injector::Error), } +// TODO: push this out of snp module +#[derive(Serialize, Deserialize, Default)] +pub struct Location { + pub node_ip: Option, + pub country: Option, + pub region: Option, + pub city: Option, +} + +impl From<&str> for Location { + fn from(s: &str) -> Self { + match s { + "Canada" => Self { country: Some("CA".to_string()), ..Default::default() }, + "Montreal" => Self { city: Some("Montréal".to_string()), ..Default::default() }, + "Vancouver" => Self { city: Some("Vancouver".to_string()), ..Default::default() }, + "US" => Self { country: Some("US".to_string()), ..Default::default() }, + "California" => Self { country: Some("US".to_string()), ..Default::default() }, + "France" => Self { country: Some("FR".to_string()), ..Default::default() }, + "GB" => Self { country: Some("GB".to_string()), ..Default::default() }, + "DE" => Self { country: Some("DE".to_string()), ..Default::default() }, + "Any" => Self { ..Default::default() }, + _ => Self { ..Default::default() }, + } + } +} + #[derive(Serialize, Default)] pub struct VmSshArgs { uuid: String, @@ -157,12 +182,12 @@ pub struct VmContract { pub uuid: String, pub hostname: String, #[tabled(rename = "Cores")] - pub vcpus: u32, - #[tabled(rename = "Mem (MB)")] - pub mem: u32, - #[tabled(rename = "Disk")] - pub disk: u32, - #[tabled(rename = "LP/h")] + pub vcpus: u64, + #[tabled(rename = "Mem", display_with = "display_mib_or_gib")] + pub mem: u64, + #[tabled(rename = "Disk", display_with = "display_mib_or_gib")] + pub disk: u64, + #[tabled(rename = "credits/h")] pub cost_h: f64, #[tabled(rename = "time left", display_with = "display_mins")] pub time_left: u64, @@ -189,9 +214,9 @@ impl From for VmContract { Self { uuid: brain_contract.uuid, hostname: brain_contract.hostname, - vcpus: brain_contract.vcpus, - mem: brain_contract.memory_mb, - disk: brain_contract.disk_size_gb, + vcpus: brain_contract.vcpus as u64, + mem: brain_contract.memory_mb as u64, + disk: brain_contract.disk_size_gb as u64, location: brain_contract.location, cost_h: (brain_contract.nano_per_minute * 60) as f64 / 1_000_000_000.0, time_left: brain_contract.locked_nano / brain_contract.nano_per_minute, @@ -201,12 +226,22 @@ impl From for VmContract { #[derive(Tabled, Debug, Serialize, Deserialize)] pub struct TabledVmNode { - #[tabled(rename = "Operator")] + #[tabled(rename = "Operator", display_with = "shorten_string")] pub operator: String, + #[tabled(rename = "Main IP")] + pub main_ip: String, #[tabled(rename = "City, Region, Country")] pub location: String, - #[tabled(rename = "IP")] - pub public_ip: String, + #[tabled(rename = "Cores")] + pub vcpus: u64, + #[tabled(rename = "Mem", display_with = "display_mib_or_gib")] + pub memory_mib: u64, + #[tabled(rename = "Disk", display_with = "display_mib_or_gib")] + pub disk_mib: u64, + #[tabled(rename = "Extra IPv4", display_with = "display_ip_support")] + pub public_ipv4: bool, + #[tabled(rename = "IPv6", display_with = "display_ip_support")] + pub public_ipv6: bool, #[tabled(rename = "Price per unit")] pub price: String, #[tabled(rename = "Reports")] @@ -218,9 +253,14 @@ impl From for TabledVmNode { Self { operator: brain_node.operator, location: brain_node.city + ", " + &brain_node.region + ", " + &brain_node.country, - public_ip: brain_node.ip, - price: format!("{} nanoLP/min", brain_node.price), + main_ip: brain_node.ip, + price: format!("{} nano/min", brain_node.price), reports: brain_node.reports.len(), + vcpus: brain_node.vcpus, + memory_mib: brain_node.memory_mib, + disk_mib: brain_node.disk_mib, + public_ipv4: brain_node.public_ipv4, + public_ipv6: brain_node.public_ipv6, } } } @@ -329,21 +369,104 @@ impl super::HumanOutput for Vec { } } -pub fn print_nodes() -> Result, Error> { +pub fn search_nodes(location: Location) -> Result, Error> { log::debug!("This will support flags in the future, but we have only one node atm."); - let req = proto::VmNodeFilters { ..Default::default() }; + let req = proto::VmNodeFilters { + city: location.city.unwrap_or_default(), + country: location.country.unwrap_or_default(), + region: location.region.unwrap_or_default(), + ..Default::default() + }; Ok(block_on(grpc::get_node_list(req))?) } +#[derive(Tabled, Debug, Serialize, Deserialize)] +pub struct NodeOffer { + #[tabled(rename = "Location")] + pub location: String, + #[tabled(rename = "Cores")] + pub vcpus: u64, + #[tabled(rename = "Mem", display_with = "display_mib_or_gib")] + pub mem: u64, + #[tabled(rename = "Disk", display_with = "display_mib_or_gib")] + pub disk: u64, + #[tabled(rename = "Public IPv4", display_with = "display_ip_support")] + pub ipv4: bool, + #[tabled(rename = "Public IPv6", display_with = "display_ip_support")] + pub ipv6: bool, + #[tabled(rename = "cost/h")] + pub cost_h: f64, + #[tabled(rename = "cost/m")] + pub cost_m: f64, +} + +fn display_ip_support(support: &bool) -> String { + match support { + true => "Available".to_string(), + false => "Unavailable".to_string(), + } +} + +impl super::HumanOutput for Vec { + fn human_cli_print(&self) { + let style = tabled::settings::Style::rounded(); + let mut table = tabled::Table::new(self); + table.with(style); + println!("{table}"); + } +} + +pub fn print_node_offers(location: Location) -> Result, Error> { + log::debug!("This will support flags in the future, but we have only one node atm."); + let req = proto::VmNodeFilters { + city: location.city.unwrap_or_default(), + country: location.country.unwrap_or_default(), + region: location.region.unwrap_or_default(), + ..Default::default() + }; + let node_list = block_on(grpc::get_node_list(req))?; + let mut offers: Vec = Vec::new(); + for node in node_list.iter() { + let mem_per_cpu = node.memory_mib / node.vcpus; + let disk_per_cpu = node.disk_mib / node.vcpus; + for i in 1..node.vcpus { + let price_per_month = calculate_nanocredits( + (node.vcpus * i) as u32, + (mem_per_cpu * i) as u32, + (disk_per_cpu * i) as u32, + false, + 732, + node.price, + ) as f64 + / 1_000_000_000_f64; + let price_per_hour = price_per_month / 732_f64; + let price_per_month = (price_per_month * 100.0).round() / 100.0; + let price_per_hour = (price_per_hour * 1000.0).round() / 1000.0; + offers.push(NodeOffer { + location: node.city.clone() + ", " + &node.region + ", " + &node.country, + vcpus: i, + mem: i * mem_per_cpu, + disk: i * disk_per_cpu, + cost_h: price_per_hour, + cost_m: price_per_month, + ipv4: node.public_ipv4, + ipv6: node.public_ipv6, + }); + } + } + offers.sort_by_key(|n| n.cost_m as u64); + Ok(offers) +} + pub fn inspect_node(ip: String) -> Result { let req = proto::VmNodeFilters { ip, ..Default::default() }; Ok(block_on(grpc::get_one_node(req))?) } -pub fn calculate_nanolp( +pub fn calculate_nanocredits( vcpus: u32, memory_mb: u32, - disk_size_gb: u32, + disk_size_mib: u32, public_ipv4: bool, hours: u32, node_price: u64, @@ -351,19 +474,9 @@ pub fn calculate_nanolp( // this calculation needs to match the calculation of the network let total_units = (vcpus as u64 * 10) + ((memory_mb + 256) as u64 / 200) - + (disk_size_gb as u64 / 10) + + (disk_size_mib as u64 / 1024 / 10) + (public_ipv4 as u64 * 10); let locked_nano = hours as u64 * 60 * total_units * node_price; - eprint!( - "Node price: {}/unit/minute. Total Units for hardware requested: {}. ", - node_price as f64 / 1_000_000_000.0, - total_units, - ); - eprintln!( - "Locking {} LP (offering the VM for {} hours).", - locked_nano as f64 / 1_000_000_000.0, - hours - ); locked_nano } diff --git a/src/snp/update.rs b/src/snp/update.rs index 3a68e82..da34991 100644 --- a/src/snp/update.rs +++ b/src/snp/update.rs @@ -12,8 +12,8 @@ use log::{debug, info}; pub struct Request { hostname: String, vcpus: u32, - memory_mb: u32, - disk_size_gb: u32, + memory_mib: u32, + disk_size_mib: u32, dtrfs: Option, } @@ -34,7 +34,7 @@ impl Request { Some(Dtrfs::load_from_file(path)?) } }; - let req = Self { hostname, vcpus, memory_mb, disk_size_gb, dtrfs }; + let req = Self { hostname, vcpus, memory_mib: memory_mb, disk_size_mib: disk_size_gb, dtrfs }; if req == Self::default() { log::info!("Skipping hardware upgrade (no arguments specified)."); return Ok(()); @@ -55,7 +55,7 @@ impl Request { let updated_contract = block_on(grpc::get_contract_by_uuid(uuid))?; debug!("Got the current contract for the VM after update. {updated_contract:#?}"); - if !(self.vcpus != 0 || self.dtrfs.is_some()) { + if !(self.vcpus != 0 || self.memory_mib != 0 || self.dtrfs.is_some()) { eprintln!("vCPUs and kernel did not get modified. Secret injection is not required."); return Ok(()); } @@ -90,9 +90,9 @@ impl Request { uuid: uuid.to_string(), hostname: self.hostname.clone(), admin_pubkey: Config::get_detee_wallet()?, - disk_size_gb: self.disk_size_gb, + disk_size_mib: self.disk_size_mib * 1024, vcpus: self.vcpus, - memory_mb: self.memory_mb, + memory_mib: self.memory_mib * 1024, kernel_url, kernel_sha, dtrfs_url, diff --git a/src/utils.rs b/src/utils.rs index 9b32a71..e16ed89 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -45,6 +45,19 @@ pub fn shorten_string(my_string: &String) -> String { } } +pub fn display_mib_or_gib(value: &u64) -> String { + if *value >= 1024 { + if *value < 102400 { + let value = (value / 102) as f64; + format!("{}G", value / 10_f64) + } else { + format!("{}G", value / 1024) + } + } else { + format!("{}M", value) + } +} + #[macro_export] macro_rules! call_with_follow_redirect { (