added logic to reserve ips

This commit is contained in:
ghe0 2024-12-09 01:47:37 +02:00
parent edbaebfc56
commit b905c96261
Signed by: ghe0
GPG Key ID: 451028EE56A0FBB4
5 changed files with 385 additions and 93 deletions

96
Cargo.lock generated

@ -8,6 +8,18 @@ version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" 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]] [[package]]
name = "cidr" name = "cidr"
version = "0.3.0" version = "0.3.0"
@ -23,9 +35,36 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cidr", "cidr",
"rand",
"serde", "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]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.92" version = "1.0.92"
@ -44,6 +83,36 @@ dependencies = [
"proc-macro2", "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]] [[package]]
name = "serde" name = "serde"
version = "1.0.215" version = "1.0.215"
@ -80,3 +149,30 @@ name = "unicode-ident"
version = "1.0.14" version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 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",
]

@ -6,4 +6,5 @@ edition = "2021"
[dependencies] [dependencies]
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"
serde = { version = "1.0.215", features = ["derive"] } serde = { version = "1.0.215", features = ["derive"] }

@ -4,35 +4,47 @@ use cidr::Ipv6Cidr;
use core::net::Ipv4Addr; use core::net::Ipv4Addr;
use core::net::Ipv6Addr; use core::net::Ipv6Addr;
use std::collections::HashSet;
use std::ops::Range; use std::ops::Range;
struct Volume { pub struct Volume {
path: String, path: String,
// maximum allowed storage in GB // maximum allowed storage in GB
max_reservation: u64, max_reservation: u64,
} }
struct Interface { pub struct IPv4Range {
driver: InterfaceType, pub subnet: Ipv4Cidr,
name: String, pub gateway: Ipv4Addr,
ipv4_ranges: Vec<Ipv4Cidr>, pub reserved_addrs: HashSet<Ipv4Addr>,
reserved_v4_addrs: Vec<Ipv4Addr>, }
ipv6_ranges: Vec<Ipv6Cidr>,
reserved_v6_addrs: Vec<Ipv6Addr>, pub struct IPv6Range {
pub subnet: Ipv6Cidr,
pub gateway: Ipv6Addr,
pub reserved_addrs: HashSet<Ipv6Addr>,
}
pub struct Interface {
pub driver: InterfaceType,
pub device: String,
pub ipv4: Vec<IPv4Range>,
pub ipv6: Vec<IPv6Range>,
} }
// TODO: create mechanic to autodetect interface type // TODO: create mechanic to autodetect interface type
enum InterfaceType { pub enum InterfaceType {
MACVTAP, MACVTAP,
IPVTAP, IPVTAP,
Bridge, Bridge,
} }
struct Config { pub struct Config {
max_cores_per_vm: u64, pub max_cores_per_vm: usize,
max_vcpu_reservation: u64, pub max_vcpu_reservation: usize,
max_mem_reservation: u64, pub max_mem_reservation: usize,
network_interfaces: Vec<Interface>, pub network_interfaces: Vec<Interface>,
volumes: Vec<Volume>, pub volumes: Vec<Volume>,
public_port_range: Range<u16>, pub public_port_range: Range<u16>,
pub max_ports_per_vm: u16,
} }

@ -1,18 +1,15 @@
#![allow(dead_code)] #![allow(dead_code)]
use crate::config::Config;
use crate::constants::*; use crate::constants::*;
use anyhow::anyhow; use anyhow::anyhow;
use anyhow::Result; use anyhow::Result;
use std::collections::HashMap;
use std::collections::HashSet; use std::collections::HashSet;
use std::fs::remove_file; use std::fs::remove_file;
use std::fs::File; use std::fs::File;
use std::io::Write; use std::io::Write;
use std::ops::Range;
use std::process::Command; use std::process::Command;
type VMUUID = String; pub enum InterfaceConfig {
pub enum NIC {
// TODO: instead of QEMU userspace NAT, use iptables kernelspace NAT // TODO: instead of QEMU userspace NAT, use iptables kernelspace NAT
// in case of QEMU-base NAT, device name is not needed // in case of QEMU-base NAT, device name is not needed
NAT { device: String }, NAT { device: String },
@ -22,89 +19,154 @@ pub enum NIC {
Bridge { device: String }, Bridge { device: String },
} }
#[derive(PartialEq)]
enum IPStatus {
Available,
Reserved(VMUUID),
Blacklisted,
}
struct IPData {
interface: NIC,
status: IPStatus,
}
pub struct StoragePool { pub struct StoragePool {
path: String, path: String,
max_reservation: u64,
current_reservation: u64, current_reservation: u64,
// add mechanic to detect storage tier // add mechanic to detect storage tier
// tier: StorageTier, // tier: StorageTier,
} }
pub struct PortPool {
port_range: Range<u16>,
used_ports: HashSet<u16>,
}
pub struct Resources { pub struct Resources {
// QEMU does not support MHz limiation // QEMU does not support MHz limiation
mac_vcpus_reservation: u64, reserved_vcpus: usize,
available_vcpus_reservation: u64, reserved_memory: usize,
total_memory_reservation: u64, used_ports: HashSet<u16>,
available_memory_reservation: u64,
// will be only one StoragePool for now and multiple later
storage_pools: Vec<StoragePool>, storage_pools: Vec<StoragePool>,
ipv4_pool: HashMap<String, IPData>, reserved_ipv4: HashSet<String>,
ipv6_pool: HashMap<String, IPData>, reserved_ipv6: HashSet<String>,
reserved_if_names: HashSet<String>,
} }
impl Resources { impl Resources {
fn get_available_ipv4(&self) -> usize { fn reserve_vm_if(&mut self) -> String {
self.ipv4_pool use rand::{distributions::Alphanumeric, Rng};
.values() loop {
.filter(|ip_data| ip_data.status == IPStatus::Available) let mut interface_name: String = rand::thread_rng()
.count() .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()
} }
} }
impl NIC { fn reserve_public_ipv4(&mut self, config: &Config) -> Option<VMNIC> {
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<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())
{
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 InterfaceConfig {
fn if_type(&self) -> String { fn if_type(&self) -> String {
match self { match self {
NIC::IPVTAP { .. } => format!("ipvtap"), InterfaceConfig::IPVTAP { .. } => format!("ipvtap"),
NIC::MACVTAP { .. } => format!("macvtap"), InterfaceConfig::MACVTAP { .. } => format!("macvtap"),
NIC::NAT { .. } => format!("nat"), InterfaceConfig::NAT { .. } => format!("nat"),
NIC::Bridge { .. } => format!("bridge"), InterfaceConfig::Bridge { .. } => format!("bridge"),
} }
} }
fn device_name(&self) -> String { fn device_name(&self) -> String {
match self { match self {
NIC::IPVTAP { device, .. } => device.clone(), InterfaceConfig::IPVTAP { device, .. } => device.clone(),
NIC::MACVTAP { device, .. } => device.clone(), InterfaceConfig::MACVTAP { device, .. } => device.clone(),
NIC::NAT { device, .. } => device.clone(), InterfaceConfig::NAT { device, .. } => device.clone(),
NIC::Bridge { device, .. } => device.clone(), InterfaceConfig::Bridge { device, .. } => device.clone(),
} }
} }
fn vtap_name(&self) -> Option<String> { fn vtap_name(&self) -> Option<String> {
match self { match self {
NIC::IPVTAP { name, .. } => Some(name.clone()), InterfaceConfig::IPVTAP { name, .. } => Some(name.clone()),
NIC::MACVTAP { name, .. } => Some(name.clone()), InterfaceConfig::MACVTAP { name, .. } => Some(name.clone()),
NIC::NAT { .. } | NIC::Bridge { .. } => None, InterfaceConfig::NAT { .. } | InterfaceConfig::Bridge { .. } => None,
} }
} }
fn is_vtap(&self) -> bool { fn is_vtap(&self) -> bool {
match self { match self {
NIC::IPVTAP { .. } | NIC::MACVTAP { .. } => true, InterfaceConfig::IPVTAP { .. } | InterfaceConfig::MACVTAP { .. } => true,
NIC::NAT { .. } | NIC::Bridge { .. } => false, InterfaceConfig::NAT { .. } | InterfaceConfig::Bridge { .. } => false,
} }
} }
} }
@ -114,42 +176,152 @@ struct IPConfig {
// requires short format (example: 24) // requires short format (example: 24)
subnet: String, subnet: String,
gateway: String, gateway: String,
nameserver: String, }
nic: NIC,
pub struct VMNIC {
if_config: InterfaceConfig,
ips: Vec<IPConfig>,
} }
pub struct VM { pub struct VM {
uuid: VMUUID, uuid: String,
hostname: String, hostname: String,
admin_key: String, admin_key: String,
ips: Vec<IPConfig>, fw_ports: Vec<u16>,
nics: Vec<VMNIC>,
// currently hardcoded to EPYC-v4 // currently hardcoded to EPYC-v4
// cpu_type: String, // cpu_type: String,
vcpus: u32, vcpus: usize,
// memory in MB // memory in MB
memory: u32, memory: usize,
// disk size in GB // disk size in GB
disk_size: u32, disk_size: usize,
kernel_path: String, kernel_path: String,
kernel_sha: String,
initrd_path: String, initrd_path: String,
ovmf_path: String, initrd_sha: String,
}
pub struct NewVMRequest {
uuid: String,
hostname: String,
admin_key: String,
forwarded_ports: Vec<u16>,
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 { impl VM {
pub fn new(
req: NewVMRequest,
config: &Config,
res: &mut Resources,
) -> Result<Self, VMCreationErrors> {
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. // For the MVP, the T-Contract offers VM+IP+Disk as a bundle.
// This means we can enforce the path to the disk. // 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. // This may change in the future as the VM is allowed to have multiple disks.
pub fn disk_path(&self) -> String { pub fn disk_path(&self) -> String {
VM_DISK_DIR.to_string() + "/" + &self.uuid + ".qcow2" VM_DISK_DIR.to_string() + "/" + &self.uuid + ".qcow2"
} }
pub fn kernel_params(&self) -> String { pub fn kernel_params(&self) -> String {
let mut ip_string = String::new(); let mut ip_string = String::new();
let mut i = 0; let mut i = 0;
for ip in self.ips.iter() { for nic in self.nics.iter() {
for ip in nic.ips.iter() {
ip_string += &format!( ip_string += &format!(
"detee_net_eth{}={}_{}_{}_{}", "detee_net_eth{}={}_{}_{}",
i, ip.address, ip.subnet, ip.gateway, ip.nameserver i, ip.address, ip.subnet, ip.gateway
); );
}
i += 1; i += 1;
} }
let admin_key = format!("detee_admin={}", self.admin_key); let admin_key = format!("detee_admin={}", self.admin_key);
@ -157,16 +329,16 @@ impl VM {
format!("{} {} {}", ip_string, admin_key, hostname) 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 vars = String::new();
let mut i = 0; let mut i = 0;
for ip in self.ips.iter() { for nic in self.nics.iter() {
let mut interface = String::new(); 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 // device is currently ignored in case of NAT cause we assume QEMU userspace NAT
if let Some(vtap_name) = ip.nic.vtap_name() { if let Some(vtap_name) = nic.if_config.vtap_name() {
interface += &format!("_{}_{}", ip.nic.device_name(), vtap_name); interface += &format!("_{}_{}", nic.if_config.device_name(), vtap_name);
} }
vars += &format!("{}\n", interface); vars += &format!("{}\n", interface);
i += 1; i += 1;
@ -182,12 +354,19 @@ impl VM {
vars += &format!("DISK={}\n", self.disk_path()); vars += &format!("DISK={}\n", self.disk_path());
vars += &format!("DISK_SIZE={}GB\n", self.disk_size); 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<()> { pub fn delete_vtap_interfaces(&self) -> Result<()> {
for ip in self.ips.iter() { for nic in self.nics.iter() {
if let Some(name) = ip.nic.vtap_name() { if let Some(name) = nic.if_config.vtap_name() {
let result = Command::new("ip") let result = Command::new("ip")
.arg("link") .arg("link")
.arg("del") .arg("del")
@ -222,13 +401,13 @@ impl VM {
contents += &format!("[Install]"); contents += &format!("[Install]");
contents += &format!("WantedBy=multi-user.target"); 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())?; file.write_all(contents.as_bytes())?;
Ok(()) Ok(())
} }
pub fn delete_systemd_unit_file(&self) -> Result<()> { 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(()) Ok(())
} }

@ -5,6 +5,10 @@ pub struct FinalizedTContract {
pub owner: String, pub owner: String,
pub user: String, pub user: String,
pub alloc: ResourceAllocation, pub alloc: ResourceAllocation,
pub kernel_uri: String,
pub kernel_sha: String,
pub initrd_uri: String,
pub initrd_sha: String,
} }
#[derive(Default)] #[derive(Default)]