create new VM from tcontract
This commit is contained in:
		
							parent
							
								
									57801c725d
								
							
						
					
					
						commit
						3109edc085
					
				
							
								
								
									
										1439
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										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" | ||||
|  | ||||
							
								
								
									
										192
									
								
								src/state.rs
									
									
									
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										192
									
								
								src/state.rs
									
									
									
									
									
								
							| @ -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,10 +342,27 @@ 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, | ||||
|             hostname: req.hostname, | ||||
| @ -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)] | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user