create new VM from tcontract

This commit is contained in:
ghe0 2024-12-10 00:20:11 +02:00
parent 57801c725d
commit 3109edc085
Signed by: ghe0
GPG Key ID: 451028EE56A0FBB4
4 changed files with 1591 additions and 46 deletions

1439
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -7,4 +7,6 @@ edition = "2021"
anyhow = "1.0.94" anyhow = "1.0.94"
cidr = { version = "0.3.0", features = ["serde"] } cidr = { version = "0.3.0", features = ["serde"] }
rand = "0.8.5" rand = "0.8.5"
reqwest = { version = "0.12.9", features = ["blocking"] }
serde = { version = "1.0.215", features = ["derive"] } serde = { version = "1.0.215", features = ["derive"] }
sha2 = "0.10.8"

@ -3,60 +3,49 @@ use crate::config::Config;
use crate::constants::*; use crate::constants::*;
use anyhow::anyhow; use anyhow::anyhow;
use anyhow::Result; use anyhow::Result;
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::collections::HashSet; use std::collections::HashSet;
use std::fs;
use std::fs::remove_file; use std::fs::remove_file;
use std::fs::File; use std::fs::File;
use std::io::Read;
use std::io::Write; use std::io::Write;
use std::path::Path;
use std::process::Command; use std::process::Command;
pub enum InterfaceConfig {
// TODO: instead of QEMU userspace NAT, use iptables kernelspace NAT
// in case of QEMU-base NAT, device name is not needed
NAT { device: String },
// TODO: figure how to calculate IF_NAME based on index
MACVTAP { name: String, device: String },
IPVTAP { name: String, device: String },
Bridge { device: String },
}
pub struct StoragePool {
path: String,
current_reservation: u64,
// add mechanic to detect storage tier
// tier: StorageTier,
}
pub struct Resources { pub struct Resources {
// QEMU does not support MHz limiation // QEMU does not support MHz limiation
reserved_vcpus: usize, reserved_vcpus: usize,
reserved_memory: usize, reserved_memory: usize,
used_ports: HashSet<u16>, reserved_ports: HashSet<u16>,
storage_pools: Vec<StoragePool>, storage_pools: Vec<StoragePool>,
reserved_ipv4: HashSet<String>, reserved_ips: HashSet<String>,
reserved_ipv6: HashSet<String>,
reserved_if_names: HashSet<String>, reserved_if_names: HashSet<String>,
// sha256sum -> absolute path
boot_files: HashMap<String, String>,
} }
impl Resources { impl Resources {
// TODO: improve this to error out if ports are not available fn reserve_ports(&mut self, nr: u16, config: &Config) -> Vec<u16> {
// be careful with loops
// server must provide number of ports in the contract or fail
fn reserve_ports(&mut self, mut nr: u16, config: &Config) -> Vec<u16> {
use rand::Rng; use rand::Rng;
if config.public_port_range.len() < self.reserved_ports.len() + nr as usize {
return Vec::new();
}
if nr > config.max_ports_per_vm { if nr > config.max_ports_per_vm {
nr = config.max_ports_per_vm; return Vec::new();
} }
let mut published_ports = Vec::new(); let mut published_ports = Vec::new();
for _ in 0..nr { for _ in 0..nr {
for _ in 0..5 { for _ in 0..5 {
let port = rand::thread_rng().gen_range(config.public_port_range.clone()); let port = rand::thread_rng().gen_range(config.public_port_range.clone());
if self.used_ports.insert(port) { if self.reserved_ports.insert(port) {
published_ports.push(port); published_ports.push(port);
} }
break; break;
} }
} }
Vec::new() published_ports
} }
fn reserve_vm_if(&mut self) -> String { fn reserve_vm_if(&mut self) -> String {
@ -79,7 +68,7 @@ impl Resources {
for range in nic.ipv4.iter() { for range in nic.ipv4.iter() {
for ip in range.subnet.iter() { for ip in range.subnet.iter() {
if !range.reserved_addrs.contains(&ip.address()) if !range.reserved_addrs.contains(&ip.address())
&& !self.reserved_ipv4.contains(&ip.to_string()) && !self.reserved_ips.contains(&ip.to_string())
{ {
let if_config = match nic.driver { let if_config = match nic.driver {
crate::config::InterfaceType::MACVTAP => InterfaceConfig::MACVTAP { crate::config::InterfaceType::MACVTAP => InterfaceConfig::MACVTAP {
@ -115,12 +104,13 @@ impl Resources {
None None
} }
// TODO: refactor this garbage cause it's only one char different from the previous one
fn reserve_public_ipv6(&mut self, config: &Config) -> Option<VMNIC> { fn reserve_public_ipv6(&mut self, config: &Config) -> Option<VMNIC> {
for nic in config.network_interfaces.iter() { for nic in config.network_interfaces.iter() {
for range in nic.ipv6.iter() { for range in nic.ipv6.iter() {
for ip in range.subnet.iter() { for ip in range.subnet.iter() {
if !range.reserved_addrs.contains(&ip.address()) if !range.reserved_addrs.contains(&ip.address())
&& !self.reserved_ipv6.contains(&ip.to_string()) && !self.reserved_ips.contains(&ip.to_string())
{ {
let if_config = match nic.driver { let if_config = match nic.driver {
crate::config::InterfaceType::MACVTAP => InterfaceConfig::MACVTAP { crate::config::InterfaceType::MACVTAP => InterfaceConfig::MACVTAP {
@ -155,6 +145,48 @@ impl Resources {
} }
None None
} }
fn scan_boot_files(&mut self) -> Result<()> {
for entry in fs::read_dir(VM_BOOT_DIR)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
match compute_sha256(&path) {
Ok(hash) => {
self.boot_files
.insert(hash, path.to_string_lossy().to_string());
}
Err(e) => return Err(anyhow!("Error computing hash for {:?}: {}", path, e)),
}
}
}
Ok(())
}
fn download_boot_file(&mut self, url: String, sha: String) -> Result<()> {
if !self.boot_files.contains_key(&sha) {
download_and_check_sha(&url, &sha)?;
}
self.boot_files.insert(sha, url);
Ok(())
}
}
pub struct StoragePool {
path: String,
current_reservation: u64,
// add mechanic to detect storage tier
// tier: StorageTier,
}
pub enum InterfaceConfig {
// TODO: instead of QEMU userspace NAT, use iptables kernelspace NAT
// in case of QEMU-base NAT, device name is not needed
NAT { device: String },
// TODO: figure how to calculate IF_NAME based on index
MACVTAP { name: String, device: String },
IPVTAP { name: String, device: String },
Bridge { device: String },
} }
impl InterfaceConfig { impl InterfaceConfig {
@ -223,36 +255,36 @@ pub struct VM {
memory: usize, memory: usize,
// disk size in GB // disk size in GB
disk_size: usize, disk_size: usize,
kernel_path: String,
kernel_sha: String, kernel_sha: String,
initrd_path: String, dtrfs_sha: String,
initrd_sha: String,
} }
pub struct NewVMRequest { pub struct NewVMRequest {
uuid: String, uuid: String,
hostname: String, hostname: String,
admin_key: String, admin_key: String,
forwarded_ports: Vec<u16>, nr_of_fw_ports: u16,
public_ipv4: bool, public_ipv4: bool,
public_ipv6: bool, public_ipv6: bool,
disk_size: usize, disk_size: usize,
vcpus: usize, vcpus: usize,
memory: usize, memory: usize,
kernel_path: String, kernel_url: String,
kernel_sha: String, kernel_sha: String,
initrd_path: String, dtrfs_url: String,
initrd_sha: String, dtrfs_sha: String,
} }
pub enum VMCreationErrors { pub enum VMCreationErrors {
NATandIPv4Conflict, NATandIPv4Conflict,
TooManyCores, TooManyCores,
NotEnoughPorts,
NotEnoughCPU, NotEnoughCPU,
NotEnoughMemory, NotEnoughMemory,
NotEnoughStorage, NotEnoughStorage,
IPv4NotAvailable, IPv4NotAvailable,
IPv6NotAvailable, IPv6NotAvailable,
BootFileError(String),
} }
impl VM { impl VM {
@ -261,7 +293,7 @@ impl VM {
config: &Config, config: &Config,
res: &mut Resources, res: &mut Resources,
) -> Result<Self, VMCreationErrors> { ) -> Result<Self, VMCreationErrors> {
if req.forwarded_ports.len() > 0 && req.public_ipv4 { if req.nr_of_fw_ports > 0 && req.public_ipv4 {
return Err(VMCreationErrors::NATandIPv4Conflict); return Err(VMCreationErrors::NATandIPv4Conflict);
} }
if config.max_cores_per_vm < req.vcpus { if config.max_cores_per_vm < req.vcpus {
@ -273,6 +305,21 @@ impl VM {
if config.max_mem_reservation < res.reserved_memory.saturating_add(req.memory) { if config.max_mem_reservation < res.reserved_memory.saturating_add(req.memory) {
return Err(VMCreationErrors::NotEnoughMemory); return Err(VMCreationErrors::NotEnoughMemory);
} }
if let Err(kernel_file_error) =
res.download_boot_file(req.kernel_url, req.kernel_sha.clone())
{
return Err(VMCreationErrors::BootFileError(format!(
"Could not get kernel: {kernel_file_error:?}"
)));
};
if let Err(dtrfs_file_error) = res.download_boot_file(req.dtrfs_url, req.dtrfs_sha.clone())
{
return Err(VMCreationErrors::BootFileError(format!(
"Could not get kernel: {dtrfs_file_error:?}"
)));
};
let mut vm_nics = Vec::new(); let mut vm_nics = Vec::new();
if req.public_ipv4 { if req.public_ipv4 {
match res.reserve_public_ipv4(config) { match res.reserve_public_ipv4(config) {
@ -295,10 +342,27 @@ impl VM {
vm_nics.push(vmnic); vm_nics.push(vmnic);
} }
} }
None => return Err(VMCreationErrors::IPv4NotAvailable), None => {
if let Some(existing_vmnic) = vm_nics.pop() {
for ip in existing_vmnic.ips {
res.reserved_ips.remove(&ip.address);
}
}
return Err(VMCreationErrors::IPv4NotAvailable);
}
} }
} }
let fw_ports = res.reserve_ports(req.nr_of_fw_ports, &config);
if fw_ports.len() == 0 {
for nic in vm_nics {
for ip in nic.ips {
res.reserved_ips.remove(&ip.address);
}
}
return Err(VMCreationErrors::NotEnoughPorts);
}
Ok(VM { Ok(VM {
uuid: req.uuid, uuid: req.uuid,
hostname: req.hostname, hostname: req.hostname,
@ -308,10 +372,8 @@ impl VM {
memory: req.memory, memory: req.memory,
disk_size: req.disk_size, disk_size: req.disk_size,
kernel_sha: req.kernel_sha, kernel_sha: req.kernel_sha,
initrd_sha: req.initrd_sha, dtrfs_sha: req.dtrfs_sha,
fw_ports: todo!(), fw_ports,
kernel_path: todo!(),
initrd_path: todo!(),
}) })
} }
@ -371,8 +433,8 @@ impl VM {
i += 1; i += 1;
} }
vars += &format!("KERNEL={}\n", self.kernel_path); vars += &format!("KERNEL={}\n", VM_BOOT_DIR.to_string() + "/" + &self.kernel_sha);
vars += &format!("INITRD={}\n", self.initrd_path); vars += &format!("INITRD={}\n", VM_BOOT_DIR.to_string() + "/" + &self.dtrfs_sha);
vars += &format!("PARAMS={}\n", self.kernel_params()); vars += &format!("PARAMS={}\n", self.kernel_params());
vars += &format!("CPU_TYPE={}\n", QEMU_VM_CPU_TYPE); vars += &format!("CPU_TYPE={}\n", QEMU_VM_CPU_TYPE);
vars += &format!("VCPUS={}\n", self.vcpus); vars += &format!("VCPUS={}\n", self.vcpus);
@ -460,3 +522,45 @@ impl VM {
Ok(()) Ok(())
} }
} }
fn download_and_check_sha(url: &str, sha: &str) -> Result<()> {
use reqwest::blocking::get;
use std::fs::File;
use std::io::copy;
use std::path::Path;
let save_path = VM_BOOT_DIR.to_string() + "/" + sha;
let response = get(url)?;
if !response.status().is_success() {
return Err(anyhow!("Failed to download file: {}", response.status()));
}
let mut file = File::create(Path::new(&save_path))?;
copy(&mut response.bytes()?.as_ref(), &mut file)?;
match compute_sha256(&save_path) {
Ok(hash) => {
if hash != sha {
return Err(anyhow!(
"Sha of the downloaded file did not match supplised sha: {} vs {}",
hash,
sha
));
}
}
Err(e) => return Err(anyhow!("Error computing hash for {:?}: {}", save_path, e)),
}
Ok(())
}
fn compute_sha256<P: AsRef<Path>>(path: P) -> Result<String> {
let mut file = fs::File::open(path)?;
let mut hasher = Sha256::new();
let mut buffer = [0u8; 8192];
loop {
let bytes_read = file.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
hasher.update(&buffer[..bytes_read]);
}
let result = hasher.finalize();
Ok(format!("{:x}", result))
}

@ -7,8 +7,8 @@ pub struct FinalizedTContract {
pub alloc: ResourceAllocation, pub alloc: ResourceAllocation,
pub kernel_uri: String, pub kernel_uri: String,
pub kernel_sha: String, pub kernel_sha: String,
pub initrd_uri: String, pub dtrfs_uri: String,
pub initrd_sha: String, pub dtrfs_sha: String,
} }
#[derive(Default)] #[derive(Default)]