#![allow(dead_code)] 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 struct Resources { // QEMU does not support MHz limiation reserved_vcpus: usize, reserved_memory: usize, reserved_ports: HashSet, storage_pools: Vec, reserved_ips: HashSet, reserved_if_names: HashSet, // sha256sum -> absolute path boot_files: HashMap, } impl Resources { fn reserve_ports(&mut self, nr: u16, config: &Config) -> Vec { 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 { 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.reserved_ports.insert(port) { published_ports.push(port); } break; } } published_ports } fn reserve_vm_if(&mut self) -> String { use rand::{distributions::Alphanumeric, Rng}; loop { let mut interface_name: String = rand::thread_rng() .sample_iter(&Alphanumeric) .take(9) .map(char::from) .collect(); interface_name = "detee".to_string() + &interface_name; if !self.reserved_if_names.contains(&interface_name) { return interface_name; } } } fn reserve_public_ipv4(&mut self, config: &Config) -> Option { for nic in config.network_interfaces.iter() { for range in nic.ipv4.iter() { for ip in range.subnet.iter() { if !range.reserved_addrs.contains(&ip.address()) && !self.reserved_ips.contains(&ip.to_string()) { let if_config = match nic.driver { crate::config::InterfaceType::MACVTAP => InterfaceConfig::MACVTAP { name: self.reserve_vm_if(), device: nic.device.clone(), }, crate::config::InterfaceType::IPVTAP => InterfaceConfig::IPVTAP { name: self.reserve_vm_if(), device: nic.device.clone(), }, crate::config::InterfaceType::Bridge => InterfaceConfig::Bridge { device: nic.device.clone(), }, }; let mut ips = Vec::new(); let mask = ip .network() .to_string() .split('/') .next() .unwrap() .to_string(); ips.push(IPConfig { address: ip.address().to_string(), subnet: mask, gateway: range.gateway.to_string(), }); return Some(VMNIC { if_config, ips }); } } } } 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 { 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_ips.contains(&ip.to_string()) { let if_config = match nic.driver { crate::config::InterfaceType::MACVTAP => InterfaceConfig::MACVTAP { name: self.reserve_vm_if(), device: nic.device.clone(), }, crate::config::InterfaceType::IPVTAP => InterfaceConfig::IPVTAP { name: self.reserve_vm_if(), device: nic.device.clone(), }, crate::config::InterfaceType::Bridge => InterfaceConfig::Bridge { device: nic.device.clone(), }, }; let mut ips = Vec::new(); let mask = ip .network() .to_string() .split('/') .next() .unwrap() .to_string(); ips.push(IPConfig { address: ip.address().to_string(), subnet: mask, gateway: range.gateway.to_string(), }); return Some(VMNIC { if_config, ips }); } } } } 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 { fn if_type(&self) -> String { match self { InterfaceConfig::IPVTAP { .. } => format!("ipvtap"), InterfaceConfig::MACVTAP { .. } => format!("macvtap"), InterfaceConfig::NAT { .. } => format!("nat"), InterfaceConfig::Bridge { .. } => format!("bridge"), } } fn device_name(&self) -> String { match self { InterfaceConfig::IPVTAP { device, .. } => device.clone(), InterfaceConfig::MACVTAP { device, .. } => device.clone(), InterfaceConfig::NAT { device, .. } => device.clone(), InterfaceConfig::Bridge { device, .. } => device.clone(), } } fn vtap_name(&self) -> Option { match self { InterfaceConfig::IPVTAP { name, .. } => Some(name.clone()), InterfaceConfig::MACVTAP { name, .. } => Some(name.clone()), InterfaceConfig::NAT { .. } | InterfaceConfig::Bridge { .. } => None, } } fn is_vtap(&self) -> bool { match self { InterfaceConfig::IPVTAP { .. } | InterfaceConfig::MACVTAP { .. } => true, InterfaceConfig::NAT { .. } | InterfaceConfig::Bridge { .. } => false, } } } struct IPConfig { address: String, // requires short format (example: 24) subnet: String, gateway: String, } pub struct VMNIC { if_config: InterfaceConfig, ips: Vec, } impl VMNIC { fn new() -> VMNIC { todo!("implement this here to improve code elegance of resource reservation"); } } pub struct VM { uuid: String, hostname: String, admin_key: String, fw_ports: Vec, nics: Vec, // currently hardcoded to EPYC-v4 // cpu_type: String, vcpus: usize, // memory in MB memory: usize, // disk size in GB disk_size: usize, kernel_sha: String, dtrfs_sha: String, } pub struct NewVMRequest { uuid: String, hostname: String, admin_key: String, nr_of_fw_ports: u16, public_ipv4: bool, public_ipv6: bool, disk_size: usize, vcpus: usize, memory: usize, kernel_url: String, kernel_sha: String, dtrfs_url: String, dtrfs_sha: String, } pub enum VMCreationErrors { NATandIPv4Conflict, TooManyCores, NotEnoughPorts, NotEnoughCPU, NotEnoughMemory, NotEnoughStorage, IPv4NotAvailable, IPv6NotAvailable, BootFileError(String), } impl VM { pub fn new( req: NewVMRequest, config: &Config, res: &mut Resources, ) -> Result { if req.nr_of_fw_ports > 0 && req.public_ipv4 { return Err(VMCreationErrors::NATandIPv4Conflict); } if config.max_cores_per_vm < req.vcpus { return Err(VMCreationErrors::TooManyCores); } if config.max_vcpu_reservation < res.reserved_vcpus.saturating_add(req.vcpus) { return Err(VMCreationErrors::NotEnoughCPU); } 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) { Some(vmnic) => vm_nics.push(vmnic), None => return Err(VMCreationErrors::IPv4NotAvailable), } } if req.public_ipv6 { match res.reserve_public_ipv4(config) { Some(mut vmnic) => { if let Some(mut existing_vmnic) = vm_nics.pop() { if vmnic.if_config.device_name() == existing_vmnic.if_config.device_name() { vmnic.ips.append(&mut existing_vmnic.ips); vm_nics.push(vmnic); } else { vm_nics.push(existing_vmnic); vm_nics.push(vmnic); } } else { vm_nics.push(vmnic); } } 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, hostname: req.hostname, admin_key: req.admin_key, nics: vm_nics, vcpus: req.vcpus, memory: req.memory, disk_size: req.disk_size, kernel_sha: req.kernel_sha, dtrfs_sha: req.dtrfs_sha, fw_ports, }) } pub fn deploy(&self) -> Result<()> { self.create_disk()?; self.write_vm_config()?; self.write_systemd_unit_file()?; // TODO: daemon-reload, systemctl enable, systemctl start Ok(()) } pub fn delete(&self) -> Result<()> { // TODO: systemctl disable, systemctl stop self.delete_disk()?; self.delete_vtap_interfaces()?; self.delete_systemd_unit_file()?; // TODO: systemctl daemon-reload Ok(()) } // For the MVP, the T-Contract offers VM+IP+Disk as a bundle. // This means we can enforce the path to the disk. // This may change in the future as the VM is allowed to have multiple disks. pub fn disk_path(&self) -> String { VM_DISK_DIR.to_string() + "/" + &self.uuid + ".qcow2" } pub fn kernel_params(&self) -> String { let mut ip_string = String::new(); let mut i = 0; for nic in self.nics.iter() { for ip in nic.ips.iter() { ip_string += &format!( "detee_net_eth{}={}_{}_{}", i, ip.address, ip.subnet, ip.gateway ); } i += 1; } let admin_key = format!("detee_admin={}", self.admin_key); let hostname = format!("detee_name={}", self.hostname); format!("{} {} {}", ip_string, admin_key, hostname) } pub fn write_vm_config(&self) -> Result<()> { let mut vars = String::new(); let mut i = 0; for nic in self.nics.iter() { let mut interface = String::new(); interface += &format!("NETWORK_INTERFACE_{}={}", i, nic.if_config.if_type()); // device is currently ignored in case of NAT cause we assume QEMU userspace NAT if let Some(vtap_name) = nic.if_config.vtap_name() { interface += &format!("_{}_{}", nic.if_config.device_name(), vtap_name); } vars += &format!("{}\n", interface); i += 1; } 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); vars += &format!("MEMORY={}MB\n", self.memory); vars += &format!("MAX_MEMORY={}MB\n", self.memory + 256); vars += &format!("DISK={}\n", self.disk_path()); vars += &format!("DISK_SIZE={}GB\n", self.disk_size); let mut file = File::create(VM_CONFIG_DIR.to_string() + "/" + &self.uuid)?; file.write_all(vars.as_bytes())?; Ok(()) } pub fn delete_vm_config(&self) -> Result<()> { remove_file(VM_CONFIG_DIR.to_string() + "/" + &self.uuid)?; Ok(()) } pub fn delete_vtap_interfaces(&self) -> Result<()> { for nic in self.nics.iter() { if let Some(name) = nic.if_config.vtap_name() { let result = Command::new("ip") .arg("link") .arg("del") .arg(&name) .output()?; if !result.status.success() { return Err(anyhow!( "Could not delete vtap interface {:?}:\n{:?}\n{:?}", name, result.stdout, result.stderr )); } } } Ok(()) } pub fn write_systemd_unit_file(&self) -> Result<()> { let mut contents = String::new(); contents += &format!("[Unit]"); contents += &format!("Description=DeTEE {}", self.uuid); contents += &format!("After=network.target"); contents += &format!(""); contents += &format!("[Service]"); contents += &format!("Type=simple"); contents += &format!("Environment=VM_UUID={}", self.uuid); contents += &format!("ExecStart={}", START_VM_SCRIPT); contents += &format!("ExecStop=/bin/kill -s SIGINT $MAINPID"); contents += &format!("Restart=always"); contents += &format!(""); contents += &format!("[Install]"); contents += &format!("WantedBy=multi-user.target"); let mut file = File::create("/etc/systemd/system/".to_string() + &self.uuid + ".service")?; file.write_all(contents.as_bytes())?; Ok(()) } pub fn delete_systemd_unit_file(&self) -> Result<()> { remove_file("/etc/systemd/system/".to_string() + &self.uuid + ".service")?; Ok(()) } pub fn create_disk(&self) -> Result<()> { let result = Command::new("qemu-img") .arg("create") .arg("-f") .arg(self.disk_path()) .arg(self.disk_size.to_string() + "G") .output()?; if !result.status.success() { return Err(anyhow!( "Could not create VM Disk:\n{:?}\n{:?}", result.stdout, result.stderr )); } Ok(()) } pub fn delete_disk(&self) -> Result<()> { remove_file(self.disk_path())?; 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>(path: P) -> Result { 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)) }