244 lines
8.4 KiB
Rust
244 lines
8.4 KiB
Rust
// 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<u32>),
|
|
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<Dtrfs>,
|
|
pub distro: Option<Distro>,
|
|
}
|
|
|
|
impl Request {
|
|
pub fn load_from_yaml(config_path: &str) -> Result<VmSshArgs, Error> {
|
|
let new_vm_config = Self::load_from_file(config_path)?;
|
|
new_vm_config.deploy()
|
|
}
|
|
|
|
fn load_from_file(config_path: &str) -> Result<Self, Error> {
|
|
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<VmSshArgs, Error> {
|
|
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<proto::NewVmReq, Error> {
|
|
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<proto::NewVmReq> {
|
|
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<u32>, 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)
|
|
}
|
|
}
|