// SPDX-License-Identifier: Apache-2.0 use std::u64::MAX; use super::grpc::{self, proto}; use super::{injector, Distro, Dtrfs, Error, VmSshArgs, DEFAULT_ARCHLINUX, DEFAULT_DTRFS}; use crate::config::Config; use crate::utils::block_on; use log::{debug, info}; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] pub enum IPv4Config { PublishPorts(Vec), PublicIPv4, } #[derive(Serialize, Deserialize)] pub struct Request { pub hostname: String, pub hours: u32, // price per unit per minute pub price: u64, pub location: super::Location, pub ipv4: IPv4Config, pub public_ipv6: bool, pub vcpus: u32, pub memory_gib: u32, pub disk_size_gib: u32, pub dtrfs: Option, pub distro: Option, } impl Request { pub fn load_from_yaml(config_path: &str) -> Result { let new_vm_config = Self::load_from_file(config_path)?; new_vm_config.deploy() } fn load_from_file(config_path: &str) -> Result { let content = std::fs::read_to_string(config_path)?; let new_vm_config: Self = serde_yaml::from_str(&content)?; Ok(new_vm_config) } pub fn deploy(&self) -> Result { 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)); } let (kernel_sha, dtrfs_sha) = match self.dtrfs.clone() { Some(dtrfs) => (dtrfs.kernel_sha, dtrfs.dtrfs_sha), None => (DEFAULT_DTRFS.kernel_sha.clone(), DEFAULT_DTRFS.dtrfs_sha.clone()), }; let args = new_vm_resp.args.ok_or(Error::NoMeasurement)?; let measurement_args = injector::Args { vm_id: new_vm_resp.vm_id.clone(), vcpus, kernel: kernel_sha, initrd: dtrfs_sha, args: args.clone(), }; let measurement = measurement_args.get_measurement()?; let (template_url, template_sha) = match &self.distro { Some(distro) => (distro.template_url.clone(), distro.template_sha.clone()), None => { (DEFAULT_ARCHLINUX.template_url.clone(), DEFAULT_ARCHLINUX.template_sha.clone()) } }; let mut ssh_args = injector::execute( measurement, args.dtrfs_api_endpoint.clone(), Some((&template_url, &template_sha)), &self.hostname, )?; ssh_args.vm_id = new_vm_resp.vm_id; ssh_args.hostname = self.hostname.clone(); let _ = super::append_vm_id_list(&ssh_args.vm_id, &ssh_args.hostname); Ok(ssh_args) } /// 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: proto::NewVmReq = proto::NewVmReq { locked_nano: MAX, ..Default::default() }; while let Some(node) = node_list_iter.next() { for offer in node.offers.iter() { if let Some(new_vm_req) = self.calculate_vm_request(Config::get_detee_wallet()?, &node.node_pubkey, offer) { if new_vm_req.locked_nano < final_request.locked_nano { final_request = new_vm_req; } } } } if final_request.node_pubkey.is_empty() { return Err(Error::NoValidNodeFound); } Ok(final_request) } fn calculate_vm_request( &self, admin_pubkey: String, node_pubkey: &str, offer: &proto::VmNodeOffer, ) -> Option { if offer.vcpus == 0 { return None; } let memory_per_cpu = offer.memory_mib / offer.vcpus; let disk_per_cpu = offer.disk_mib / offer.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; if memory_mib > offer.memory_mib as u32 || disk_size_mib > offer.disk_mib as u32 || vcpus > offer.vcpus as u32 { return None; } let (extra_ports, public_ipv4): (Vec, bool) = match &self.ipv4 { IPv4Config::PublishPorts(vec) => (vec.to_vec(), false), IPv4Config::PublicIPv4 => (Vec::new(), true), }; let (kernel_url, kernel_sha, dtrfs_url, dtrfs_sha) = match self.dtrfs.clone() { Some(dtrfs) => (dtrfs.kernel_url, dtrfs.kernel_sha, dtrfs.dtrfs_url, dtrfs.dtrfs_sha), None => ( DEFAULT_DTRFS.kernel_url.clone(), DEFAULT_DTRFS.kernel_sha.clone(), DEFAULT_DTRFS.dtrfs_url.clone(), DEFAULT_DTRFS.dtrfs_sha.clone(), ), }; let nanocredits = super::calculate_nanocredits( vcpus, memory_mib, disk_size_mib, public_ipv4, self.hours, offer.price, ); let brain_req = proto::NewVmReq { vm_id: String::new(), hostname: self.hostname.clone(), admin_pubkey, node_pubkey: node_pubkey.to_string(), extra_ports, public_ipv4, public_ipv6: self.public_ipv6, disk_size_mib, vcpus, memory_mib, kernel_url, kernel_sha, dtrfs_url, dtrfs_sha, price_per_unit: offer.price, locked_nano: nanocredits, }; info!( "Node {} can offer the VM at {} nanocredits for {} hours. Spec: {} vCPUs, {} MiB mem, {} MiB disk.", node_pubkey, brain_req.locked_nano, self.hours, brain_req.vcpus, brain_req.memory_mib, brain_req.disk_size_mib ); Some(brain_req) } }