444 lines
15 KiB
Rust
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()
|
|
}
|
|
];
|
|
}
|