diff --git a/Cargo.lock b/Cargo.lock index dd88ee5..7f62f19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,18 @@ version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "cidr" version = "0.3.0" @@ -23,9 +35,36 @@ version = "0.1.0" dependencies = [ "anyhow", "cidr", + "rand", "serde", ] +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "libc" +version = "0.2.167" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.92" @@ -44,6 +83,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "serde" version = "1.0.215" @@ -80,3 +149,30 @@ name = "unicode-ident" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 0ce230b..c7d13ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,4 +6,5 @@ edition = "2021" [dependencies] anyhow = "1.0.94" cidr = { version = "0.3.0", features = ["serde"] } +rand = "0.8.5" serde = { version = "1.0.215", features = ["derive"] } diff --git a/src/config.rs b/src/config.rs index 2954b2d..af4f9a3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,35 +4,47 @@ use cidr::Ipv6Cidr; use core::net::Ipv4Addr; use core::net::Ipv6Addr; +use std::collections::HashSet; use std::ops::Range; -struct Volume { +pub struct Volume { path: String, // maximum allowed storage in GB max_reservation: u64, } -struct Interface { - driver: InterfaceType, - name: String, - ipv4_ranges: Vec, - reserved_v4_addrs: Vec, - ipv6_ranges: Vec, - reserved_v6_addrs: Vec, +pub struct IPv4Range { + pub subnet: Ipv4Cidr, + pub gateway: Ipv4Addr, + pub reserved_addrs: HashSet, +} + +pub struct IPv6Range { + pub subnet: Ipv6Cidr, + pub gateway: Ipv6Addr, + pub reserved_addrs: HashSet, +} + +pub struct Interface { + pub driver: InterfaceType, + pub device: String, + pub ipv4: Vec, + pub ipv6: Vec, } // TODO: create mechanic to autodetect interface type -enum InterfaceType { +pub enum InterfaceType { MACVTAP, IPVTAP, Bridge, } -struct Config { - max_cores_per_vm: u64, - max_vcpu_reservation: u64, - max_mem_reservation: u64, - network_interfaces: Vec, - volumes: Vec, - public_port_range: Range, +pub struct Config { + pub max_cores_per_vm: usize, + pub max_vcpu_reservation: usize, + pub max_mem_reservation: usize, + pub network_interfaces: Vec, + pub volumes: Vec, + pub public_port_range: Range, + pub max_ports_per_vm: u16, } diff --git a/src/state.rs b/src/state.rs index 2de7ed2..a487410 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,18 +1,15 @@ #![allow(dead_code)] +use crate::config::Config; use crate::constants::*; use anyhow::anyhow; use anyhow::Result; -use std::collections::HashMap; use std::collections::HashSet; use std::fs::remove_file; use std::fs::File; use std::io::Write; -use std::ops::Range; use std::process::Command; -type VMUUID = String; - -pub enum NIC { +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 }, @@ -22,89 +19,154 @@ pub enum NIC { Bridge { device: String }, } -#[derive(PartialEq)] -enum IPStatus { - Available, - Reserved(VMUUID), - Blacklisted, -} - -struct IPData { - interface: NIC, - status: IPStatus, -} - pub struct StoragePool { path: String, - max_reservation: u64, current_reservation: u64, // add mechanic to detect storage tier // tier: StorageTier, } -pub struct PortPool { - port_range: Range, - used_ports: HashSet, -} - pub struct Resources { // QEMU does not support MHz limiation - mac_vcpus_reservation: u64, - available_vcpus_reservation: u64, - total_memory_reservation: u64, - available_memory_reservation: u64, - // will be only one StoragePool for now and multiple later + reserved_vcpus: usize, + reserved_memory: usize, + used_ports: HashSet, storage_pools: Vec, - ipv4_pool: HashMap, - ipv6_pool: HashMap, + reserved_ipv4: HashSet, + reserved_ipv6: HashSet, + reserved_if_names: HashSet, } impl Resources { - fn get_available_ipv4(&self) -> usize { - self.ipv4_pool - .values() - .filter(|ip_data| ip_data.status == IPStatus::Available) - .count() + 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 get_available_ipv6(&self) -> usize { - self.ipv6_pool - .values() - .filter(|ip_data| ip_data.status == IPStatus::Available) - .count() + + 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_ipv4.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 = range + .subnet + .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 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_ipv6.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 = range + .subnet + .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 } } -impl NIC { +impl InterfaceConfig { fn if_type(&self) -> String { match self { - NIC::IPVTAP { .. } => format!("ipvtap"), - NIC::MACVTAP { .. } => format!("macvtap"), - NIC::NAT { .. } => format!("nat"), - NIC::Bridge { .. } => format!("bridge"), + InterfaceConfig::IPVTAP { .. } => format!("ipvtap"), + InterfaceConfig::MACVTAP { .. } => format!("macvtap"), + InterfaceConfig::NAT { .. } => format!("nat"), + InterfaceConfig::Bridge { .. } => format!("bridge"), } } fn device_name(&self) -> String { match self { - NIC::IPVTAP { device, .. } => device.clone(), - NIC::MACVTAP { device, .. } => device.clone(), - NIC::NAT { device, .. } => device.clone(), - NIC::Bridge { device, .. } => device.clone(), + 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 { - NIC::IPVTAP { name, .. } => Some(name.clone()), - NIC::MACVTAP { name, .. } => Some(name.clone()), - NIC::NAT { .. } | NIC::Bridge { .. } => None, + InterfaceConfig::IPVTAP { name, .. } => Some(name.clone()), + InterfaceConfig::MACVTAP { name, .. } => Some(name.clone()), + InterfaceConfig::NAT { .. } | InterfaceConfig::Bridge { .. } => None, } } fn is_vtap(&self) -> bool { match self { - NIC::IPVTAP { .. } | NIC::MACVTAP { .. } => true, - NIC::NAT { .. } | NIC::Bridge { .. } => false, + InterfaceConfig::IPVTAP { .. } | InterfaceConfig::MACVTAP { .. } => true, + InterfaceConfig::NAT { .. } | InterfaceConfig::Bridge { .. } => false, } } } @@ -114,42 +176,152 @@ struct IPConfig { // requires short format (example: 24) subnet: String, gateway: String, - nameserver: String, - nic: NIC, +} + +pub struct VMNIC { + if_config: InterfaceConfig, + ips: Vec, } pub struct VM { - uuid: VMUUID, + uuid: String, hostname: String, admin_key: String, - ips: Vec, + fw_ports: Vec, + nics: Vec, // currently hardcoded to EPYC-v4 // cpu_type: String, - vcpus: u32, + vcpus: usize, // memory in MB - memory: u32, + memory: usize, // disk size in GB - disk_size: u32, + disk_size: usize, kernel_path: String, + kernel_sha: String, initrd_path: String, - ovmf_path: String, + initrd_sha: String, +} + +pub struct NewVMRequest { + uuid: String, + hostname: String, + admin_key: String, + forwarded_ports: Vec, + public_ipv4: bool, + public_ipv6: bool, + disk_size: usize, + vcpus: usize, + memory: usize, + kernel_path: String, + kernel_sha: String, + initrd_path: String, + initrd_sha: String, +} + +pub enum VMCreationErrors { + NATandIPv4Conflict, + TooManyCores, + NotEnoughCPU, + NotEnoughMemory, + NotEnoughStorage, + IPv4NotAvailable, + IPv6NotAvailable, } impl VM { + pub fn new( + req: NewVMRequest, + config: &Config, + res: &mut Resources, + ) -> Result { + if req.forwarded_ports.len() > 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); + } + 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 => return Err(VMCreationErrors::IPv4NotAvailable), + } + } + + 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, + initrd_sha: req.initrd_sha, + fw_ports: todo!(), + kernel_path: todo!(), + initrd_path: todo!(), + }) + } + + 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 ip in self.ips.iter() { - ip_string += &format!( - "detee_net_eth{}={}_{}_{}_{}", - i, ip.address, ip.subnet, ip.gateway, ip.nameserver - ); + 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); @@ -157,16 +329,16 @@ impl VM { format!("{} {} {}", ip_string, admin_key, hostname) } - pub fn export_vm_env(&self) -> String { + pub fn write_vm_config(&self) -> Result<()> { let mut vars = String::new(); let mut i = 0; - for ip in self.ips.iter() { + for nic in self.nics.iter() { let mut interface = String::new(); - interface += &format!("NETWORK_INTERFACE_{}={}", i, ip.nic.if_type()); + 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) = ip.nic.vtap_name() { - interface += &format!("_{}_{}", ip.nic.device_name(), vtap_name); + 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; @@ -182,12 +354,19 @@ impl VM { vars += &format!("DISK={}\n", self.disk_path()); vars += &format!("DISK_SIZE={}GB\n", self.disk_size); - todo!(); + 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 ip in self.ips.iter() { - if let Some(name) = ip.nic.vtap_name() { + for nic in self.nics.iter() { + if let Some(name) = nic.if_config.vtap_name() { let result = Command::new("ip") .arg("link") .arg("del") @@ -222,13 +401,13 @@ impl VM { contents += &format!("[Install]"); contents += &format!("WantedBy=multi-user.target"); - let mut file = File::create(VM_CONFIG_DIR.to_string() + "/" + &self.uuid)?; + 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(VM_CONFIG_DIR.to_string() + "/" + &self.uuid)?; + remove_file("/etc/systemd/system/".to_string() + &self.uuid + ".service")?; Ok(()) } diff --git a/src/tcontract.rs b/src/tcontract.rs index 0424677..adf9d11 100644 --- a/src/tcontract.rs +++ b/src/tcontract.rs @@ -5,6 +5,10 @@ pub struct FinalizedTContract { pub owner: String, pub user: String, pub alloc: ResourceAllocation, + pub kernel_uri: String, + pub kernel_sha: String, + pub initrd_uri: String, + pub initrd_sha: String, } #[derive(Default)]