detee-cli/src/snp/deploy.rs

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)
}
}