detee-cli/src/snp/mod.rs

444 lines
15 KiB
Rust

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<grpc::proto::VmContract> for VmSshArgs {
type Error = Error;
fn try_from(contract: grpc::proto::VmContract) -> Result<Self, Self::Error> {
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.vm_public_ipv4.is_empty() {
args.ip = contract.vm_public_ipv4;
args.port = "22".to_string();
} else {
args.port = contract.mapped_ports[0].host_port.to_string();
log::debug!(
"This VM does not have a public IP. Using node public IP: {}",
contract.node_ip
);
args.ip = contract.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<Self> {
vec![DEFAULT_DTRFS.clone(), ALTERNATIVE_INIT[0].clone(), ALTERNATIVE_INIT[1].clone()]
}
fn load_from_file(config_path: &str) -> Result<Self, Error> {
let content = std::fs::read_to_string(config_path)?;
Ok(serde_yaml::from_str(&content)?)
}
}
impl super::HumanOutput for Vec<Dtrfs> {
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<Distro> {
fn human_cli_print(&self) {
print!("{}", serde_yaml::to_string(&self).unwrap())
}
}
impl Distro {
pub fn get_template_list() -> Vec<Self> {
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<VmContract> {
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<proto::VmContract> for VmContract {
fn from(brain_contract: proto::VmContract) -> Self {
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: 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,
}
}
}
#[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<proto::VmNodeListResp> 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<VmSshArgs, Error> {
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<proto::VmContract, Error> {
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<proto::VmNodeListResp, Error> {
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<Vec<VmContract>, Error> {
let req = proto::ListVmContractsReq {
wallet: Config::get_detee_wallet()?,
as_operator,
..Default::default()
};
let contracts: Vec<VmContract> =
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::<Vec<String>>()
.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<proto::VmNodeListResp> {
fn human_cli_print(&self) {
let nodes: Vec<TabledVmNode> = 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<Vec<proto::VmNodeListResp>, 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<proto::VmNodeListResp, Error> {
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<Dtrfs> = 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<Distro> = 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()
}
];
}