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"
cidr = { version = "0.3.0", features = ["serde"] }
rand = "0.8.5"
reqwest = { version = "0.12.9", features = ["blocking"] }
serde = { version = "1.0.215", features = ["derive"] }
sha2 = "0.10.8"

@ -3,60 +3,49 @@ use crate::config::Config;
use crate::constants::*;
use anyhow::anyhow;
use anyhow::Result;
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::collections::HashSet;
use std::fs;
use std::fs::remove_file;
use std::fs::File;
use std::io::Read;
use std::io::Write;
use std::path::Path;
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 {
// QEMU does not support MHz limiation
reserved_vcpus: usize,
reserved_memory: usize,
used_ports: HashSet<u16>,
reserved_ports: HashSet<u16>,
storage_pools: Vec<StoragePool>,
reserved_ipv4: HashSet<String>,
reserved_ipv6: HashSet<String>,
reserved_ips: HashSet<String>,
reserved_if_names: HashSet<String>,
// sha256sum -> absolute path
boot_files: HashMap<String, String>,
}
impl Resources {
// TODO: improve this to error out if ports are not available
// 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> {
fn reserve_ports(&mut self, nr: u16, config: &Config) -> Vec<u16> {
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 {
nr = config.max_ports_per_vm;
return Vec::new();
}
let mut published_ports = Vec::new();
for _ in 0..nr {
for _ in 0..5 {
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);
}
break;
}
}
Vec::new()
published_ports
}
fn reserve_vm_if(&mut self) -> String {
@ -79,7 +68,7 @@ impl Resources {
for range in nic.ipv4.iter() {
for ip in range.subnet.iter() {
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 {
crate::config::InterfaceType::MACVTAP => InterfaceConfig::MACVTAP {
@ -115,12 +104,13 @@ impl Resources {
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> {
for nic in config.network_interfaces.iter() {
for range in nic.ipv6.iter() {
for ip in range.subnet.iter() {
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 {
crate::config::InterfaceType::MACVTAP => InterfaceConfig::MACVTAP {
@ -155,6 +145,48 @@ impl Resources {
}
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 {
@ -223,36 +255,36 @@ pub struct VM {
memory: usize,
// disk size in GB
disk_size: usize,
kernel_path: String,
kernel_sha: String,
initrd_path: String,
initrd_sha: String,
dtrfs_sha: String,
}
pub struct NewVMRequest {
uuid: String,
hostname: String,
admin_key: String,
forwarded_ports: Vec<u16>,
nr_of_fw_ports: u16,
public_ipv4: bool,
public_ipv6: bool,
disk_size: usize,
vcpus: usize,
memory: usize,
kernel_path: String,
kernel_url: String,
kernel_sha: String,
initrd_path: String,
initrd_sha: String,
dtrfs_url: String,
dtrfs_sha: String,
}
pub enum VMCreationErrors {
NATandIPv4Conflict,
TooManyCores,
NotEnoughPorts,
NotEnoughCPU,
NotEnoughMemory,
NotEnoughStorage,
IPv4NotAvailable,
IPv6NotAvailable,
BootFileError(String),
}
impl VM {
@ -261,7 +293,7 @@ impl VM {
config: &Config,
res: &mut Resources,
) -> 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);
}
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) {
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();
if req.public_ipv4 {
match res.reserve_public_ipv4(config) {
@ -295,9 +342,26 @@ impl VM {
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 {
uuid: req.uuid,
@ -308,10 +372,8 @@ impl VM {
memory: req.memory,
disk_size: req.disk_size,
kernel_sha: req.kernel_sha,
initrd_sha: req.initrd_sha,
fw_ports: todo!(),
kernel_path: todo!(),
initrd_path: todo!(),
dtrfs_sha: req.dtrfs_sha,
fw_ports,
})
}
@ -371,8 +433,8 @@ impl VM {
i += 1;
}
vars += &format!("KERNEL={}\n", self.kernel_path);
vars += &format!("INITRD={}\n", self.initrd_path);
vars += &format!("KERNEL={}\n", VM_BOOT_DIR.to_string() + "/" + &self.kernel_sha);
vars += &format!("INITRD={}\n", VM_BOOT_DIR.to_string() + "/" + &self.dtrfs_sha);
vars += &format!("PARAMS={}\n", self.kernel_params());
vars += &format!("CPU_TYPE={}\n", QEMU_VM_CPU_TYPE);
vars += &format!("VCPUS={}\n", self.vcpus);
@ -460,3 +522,45 @@ impl VM {
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 kernel_uri: String,
pub kernel_sha: String,
pub initrd_uri: String,
pub initrd_sha: String,
pub dtrfs_uri: String,
pub dtrfs_sha: String,
}
#[derive(Default)]