// SPDX-License-Identifier: Apache-2.0 pub mod cli_handler; pub mod deploy; pub mod grpc; mod injector; pub mod update; use crate::utils::block_on; use crate::utils::shorten_string; use crate::{ config::{self, Config}, snp, }; use grpc::proto; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; use tabled::Tabled; #[derive(thiserror::Error, Debug)] pub enum Error { #[error(transparent)] Config(#[from] config::Error), #[error("Could not find a contract with the ID {0}")] VmContractNotFound(String), #[error("Did not find a SNP node that matches your criteria")] NoValidNodeFound, #[error("Could not read yaml from disk: {0}")] YamlNotFound(#[from] std::io::Error), #[error("Failed to parse yaml update file: {0}")] YamlFormat(#[from] serde_yaml::Error), #[error("Brain returned the following error: {0}")] Brain(#[from] grpc::Error), #[error("Node returned the following error: {0}")] Node(String), #[error("Failed to obtain boot measurement for VM")] NoMeasurement, #[error("Failed to inject secrets: {0}")] Injector(#[from] injector::Error), } #[derive(Serialize, Default)] pub struct VmSshArgs { uuid: String, hostname: String, ip: String, port: String, user: String, key_path: String, } impl crate::HumanOutput for VmSshArgs { fn human_cli_print(&self) { println!("To SSH into {} (UUID: {}), run the following command:", self.hostname, self.uuid); println!(" ssh -i {} -p {} {}@{}", self.key_path, self.port, self.user, self.ip); } } impl TryFrom for VmSshArgs { type Error = Error; fn try_from(contract: grpc::proto::VmContract) -> Result { let mut args = VmSshArgs { ..Default::default() }; args.uuid = contract.uuid; args.hostname = contract.hostname; args.user = "root".to_string(); args.key_path = Config::init_config().get_ssh_pubkey()?.trim_end_matches(".pub").to_string(); if !contract.public_ipv4.is_empty() { args.ip = contract.public_ipv4; args.port = "22".to_string(); } else { args.port = contract.exposed_ports[0].to_string(); log::info!( "This VM does not have a public IP. Getting node IP for node {}", contract.node_pubkey ); let node = block_on(snp::grpc::get_one_node(proto::VmNodeFilters { node_pubkey: contract.node_pubkey.clone(), ..Default::default() }))?; args.ip = node.ip; } Ok(args) } } #[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] pub struct Dtrfs { #[serde(default)] name: String, #[serde(default)] vendor: String, dtrfs_url: String, dtrfs_sha: String, kernel_url: String, kernel_sha: String, } impl Dtrfs { pub fn print_dtrfs_list() -> Vec { // let mut dtrfs_vec = Vec::new(); // dtrfs_vec.push(DEFAULT_DTRFS.clone()); // dtrfs_vec.push(ALTERNATIVE_INIT[0].clone()); // dtrfs_vec.push(ALTERNATIVE_INIT[1].clone()); // dtrfs_vec vec![DEFAULT_DTRFS.clone(), ALTERNATIVE_INIT[0].clone(), ALTERNATIVE_INIT[1].clone()] } fn load_from_file(config_path: &str) -> Result { let content = std::fs::read_to_string(config_path)?; Ok(serde_yaml::from_str(&content)?) } } impl super::HumanOutput for Vec { fn human_cli_print(&self) { print!("{}", serde_yaml::to_string(&self).unwrap()) } } #[derive(Clone, Serialize, Deserialize, Debug)] pub struct Distro { name: String, vendor: String, template_url: String, template_sha: String, } impl super::HumanOutput for Vec { fn human_cli_print(&self) { print!("{}", serde_yaml::to_string(&self).unwrap()) } } impl Distro { pub fn get_template_list() -> Vec { // let mut distro_vec = Vec::new(); // distro_vec.push(DEFAULT_ARCHLINUX.clone()); // distro_vec.push(DEFAULT_UBUNTU.clone()); // distro_vec.push(DEFAULT_FEDORA.clone()); // distro_vec.push(ALTERNATIVE_DISTROS[0].clone()); // distro_vec.push(ALTERNATIVE_DISTROS[1].clone()); // distro_vec.push(ALTERNATIVE_DISTROS[2].clone()); // distro_vec vec![ DEFAULT_ARCHLINUX.clone(), DEFAULT_UBUNTU.clone(), DEFAULT_FEDORA.clone(), ALTERNATIVE_DISTROS[0].clone(), ALTERNATIVE_DISTROS[1].clone(), ALTERNATIVE_DISTROS[2].clone(), ] } pub fn from_string(distro: &str) -> Distro { match distro { "arch" => DEFAULT_ARCHLINUX.clone(), "ubuntu" => DEFAULT_UBUNTU.clone(), "fedora" => DEFAULT_FEDORA.clone(), _ => { log::error!("Only arch, fedora and ubuntu are currently available"); panic!(); } } } } #[derive(Tabled, Debug, Serialize, Deserialize)] pub struct VmContract { #[tabled(rename = "Location")] pub location: String, #[tabled(rename = "UUID pfx", display_with = "shorten_string")] 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 cost_h: f64, #[tabled(rename = "time left", display_with = "display_mins")] pub time_left: u64, } impl crate::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}"); } } fn display_mins(minutes: &u64) -> String { let mins = minutes % 60; let hours = minutes / 60; format!("{hours}h {mins}m") } impl From for VmContract { fn from(brain_contract: proto::VmContract) -> Self { let node_pubkey = brain_contract.node_pubkey.clone(); let location = match block_on(snp::grpc::get_one_node(proto::VmNodeFilters { node_pubkey: node_pubkey.clone(), ..Default::default() })) { Ok(node) => format!("{}, {} ({})", node.city, node.region, node.country), Err(e) => { log::warn!("Could not get information about node {node_pubkey} fram brain: {e:?}"); String::new() } }; Self { uuid: brain_contract.uuid, hostname: brain_contract.hostname, vcpus: brain_contract.vcpus, mem: brain_contract.memory_mb, disk: brain_contract.disk_size_gb, 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, } } } #[derive(Tabled, Debug, Serialize, Deserialize)] pub struct TabledVmNode { #[tabled(rename = "Operator")] pub operator: String, #[tabled(rename = "City, Region, Country")] pub location: String, #[tabled(rename = "IP")] pub public_ip: String, #[tabled(rename = "Price per unit")] pub price: String, #[tabled(rename = "Reports")] pub reports: usize, } impl From for TabledVmNode { fn from(brain_node: proto::VmNodeListResp) -> Self { 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), reports: brain_node.reports.len(), } } } pub fn ssh(uuid: &str, just_print: bool) -> Result { log::info!("Getting VM information about {uuid} from brain..."); let req = proto::ListVmContractsReq { wallet: Config::get_detee_wallet()?, uuid: uuid.to_string(), ..Default::default() }; let contracts = block_on(snp::grpc::list_contracts(req))?; if contracts.is_empty() { return Err(Error::VmContractNotFound(uuid.to_string())); } let args: VmSshArgs = contracts[0].clone().try_into()?; if just_print { return Ok(args); } eprintln!( "Running SSH command: ssh -i {} -p {} {}@{}", args.key_path, args.port, args.user, args.ip ); use std::os::unix::process::CommandExt; let e = std::process::Command::new("ssh") .arg("-i") .arg(args.key_path) .arg("-p") .arg(args.port) .arg(format!("{}@{}", args.user, args.ip)) .stdin(std::process::Stdio::inherit()) .stdout(std::process::Stdio::inherit()) .stderr(std::process::Stdio::inherit()) .exec(); println!("Error: Failed to execute ssh process: {e}"); std::process::exit(2); } pub fn get_one_contract(uuid: &str) -> Result { log::debug!("Getting contract {uuid} from brain..."); let req = proto::ListVmContractsReq { wallet: Config::get_detee_wallet()?, uuid: uuid.to_string(), ..Default::default() }; let contracts = block_on(snp::grpc::list_contracts(req))?; if contracts.is_empty() { return Err(Error::VmContractNotFound(uuid.to_string())); } Ok(contracts[0].clone()) } pub fn get_node_by_contract(uuid: &str) -> Result { let contract = get_one_contract(uuid)?; Ok(block_on(snp::grpc::get_one_node(proto::VmNodeFilters { node_pubkey: contract.node_pubkey, ..Default::default() }))?) } pub fn delete_contract(uuid: &str) -> Result<(), Error> { Ok(block_on(snp::grpc::delete_vm(uuid))?) } pub fn list_contracts(as_operator: bool) -> Result, Error> { let req = proto::ListVmContractsReq { wallet: Config::get_detee_wallet()?, as_operator, ..Default::default() }; let contracts: Vec = block_on(grpc::list_contracts(req))?.into_iter().map(|n| n.into()).collect(); let _ = write_uuid_list(&contracts); Ok(contracts) } fn write_uuid_list(contracts: &[VmContract]) -> Result<(), Error> { let vm_uuid_list_path = Config::vm_uuid_list_path()?; let mut file = std::fs::File::create(vm_uuid_list_path)?; let output: String = contracts .iter() .map(|vm| format!("{}\t{}", vm.uuid, vm.hostname).to_string()) .collect::>() .join("\n"); let output = output + "\n"; std::io::Write::write_all(&mut file, output.as_bytes())?; Ok(()) } pub fn append_uuid_list(uuid: &str, hostname: &str) -> Result<(), Error> { use std::{fs::OpenOptions, io::prelude::*}; let mut file = OpenOptions::new().create(true).append(true).open(Config::vm_uuid_list_path()?)?; writeln!(file, "{uuid}\t{hostname}")?; Ok(()) } impl super::HumanOutput for Vec { fn human_cli_print(&self) { let nodes: Vec = self.iter().map(|n| n.clone().into()).collect(); let style = tabled::settings::Style::rounded(); let mut table = tabled::Table::new(nodes); table.with(style); println!("{table}"); } } pub fn print_nodes() -> Result, Error> { log::debug!("This will support flags in the future, but we have only one node atm."); let req = proto::VmNodeFilters { ..Default::default() }; Ok(block_on(grpc::get_node_list(req))?) } 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( vcpus: u32, memory_mb: u32, disk_size_gb: u32, public_ipv4: bool, hours: u32, node_price: u64, ) -> u64 { // 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) + (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 } lazy_static! { static ref DEFAULT_DTRFS: Dtrfs = Dtrfs { name: "dtrfs-6.14.2-arch1-1".to_string(), vendor: "ghe0".to_string(), dtrfs_url: "http://registry.detee.ltd/detee-archtop-6.14.2-arch1-1.cpio.gz".to_string(), dtrfs_sha: "d207644ee60d54009b6ecdfb720e2ec251cde31774dd249fcc7435aca0377990".to_string(), kernel_url: "http://registry.detee.ltd/vmlinuz-linux-6.14.2-arch1-1".to_string(), kernel_sha: "e765e56166ef321b53399b9638584d1279821dbe3d46191c1f66bbaa075e7919".to_string() }; static ref DEFAULT_ARCHLINUX: Distro = Distro { name: "archlinux_2025-04-03".to_string(), vendor: "gheorghe".to_string(), template_url: "http://registry.detee.ltd/detee_arch_2025-04-03.fsa".to_string(), template_sha: "7fdb19d9325c63d246140c984dc3764538f6ea329ed877e947993ea7bc8c2067" .to_string() }; static ref DEFAULT_UBUNTU: Distro = Distro { name: "ubuntu_2025-04-03".to_string(), vendor: "gheorghe".to_string(), template_url: "http://registry.detee.ltd/detee_ubuntu_2025-04-03.fsa".to_string(), template_sha: "324895a7a1788e43253cf9699aa446df1a5519fe072917cedcc4ed356546e34a" .to_string() }; static ref DEFAULT_FEDORA: Distro = Distro { name: "fedora_2025-04-03".to_string(), vendor: "gheorghe".to_string(), template_url: "http://registry.detee.ltd/detee_fedora_2025-04-03.fsa".to_string(), template_sha: "75a98c3744552bbf5f8e9c6a271cd0f382e1d9a846f5d577767b39293b8efda9" .to_string() }; static ref ALTERNATIVE_INIT: Vec = vec![ Dtrfs { name: "dtrfs-6.13.7-arch1-1".to_string(), vendor: "ghe0".to_string(), dtrfs_url: "http://registry.detee.ltd/detee-archtop-6.13.7-arch1-1.cpio.gz".to_string(), dtrfs_sha: "dc02e091da80c281fe735a1be86b3fe766f1741d82c32f5dc64344b345827c6d" .to_string(), kernel_url: "http://registry.detee.ltd/vmlinuz-linux-6.13.7-arch1-1".to_string(), kernel_sha: "469a89668d2f5744b3f80417fcf0a4ce0140fcb78f1e8834ef8e3668eecc934c" .to_string() }, Dtrfs { name: "dtrfs-6.13.8-arch1-1".to_string(), vendor: "ghe0".to_string(), dtrfs_url: "http://registry.detee.ltd/detee-archtop-6.13.8-arch1-1.cpio.gz".to_string(), dtrfs_sha: "b5f408d00e2b93dc594fed3a7f2466a9878802ff1c7ae502247471cd06728a45" .to_string(), kernel_url: "http://registry.detee.ltd/vmlinuz-linux-6.13.8-arch1-1".to_string(), kernel_sha: "e49c8587287b21df7600c04326fd7393524453918c14d67f73757dc769a13542" .to_string() }, ]; static ref ALTERNATIVE_DISTROS: Vec = vec![ Distro { name: "archlinux_2025-02-21".to_string(), vendor: "gheorghe".to_string(), template_url: "http://registry.detee.ltd/detee_arch_2025-02-21.fsa".to_string(), template_sha: "257edbf1e3b949b895c422befc8890c85dfae1ad3d35661010c9aaa173ba9fc4" .to_string() }, Distro { name: "ubuntu_2025-02-28".to_string(), vendor: "gheorghe".to_string(), template_url: "http://registry.detee.ltd/detee_ubuntu_2025-02-28.fsa".to_string(), template_sha: "faa8bd38d02ca9b6ee69d7f5128ed9ccab42bdbfa69f688b9947e8e5c9e5d133" .to_string() }, Distro { name: "fedora_2025-02-21".to_string(), vendor: "gheorghe".to_string(), template_url: "http://registry.detee.ltd/detee_fedora_2025-02-21.fsa".to_string(), template_sha: "c0fdd08d465939077ef8ed746903005fc190af12cdf70917cc8c6f872da85777" .to_string() } ]; }