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"
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",
]

@ -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"] }

@ -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<Ipv4Cidr>,
reserved_v4_addrs: Vec<Ipv4Addr>,
ipv6_ranges: Vec<Ipv6Cidr>,
reserved_v6_addrs: Vec<Ipv6Addr>,
pub struct IPv4Range {
pub subnet: Ipv4Cidr,
pub gateway: Ipv4Addr,
pub reserved_addrs: HashSet<Ipv4Addr>,
}
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
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<Interface>,
volumes: Vec<Volume>,
public_port_range: Range<u16>,
pub struct Config {
pub max_cores_per_vm: usize,
pub max_vcpu_reservation: usize,
pub max_mem_reservation: usize,
pub network_interfaces: Vec<Interface>,
pub volumes: Vec<Volume>,
pub public_port_range: Range<u16>,
pub max_ports_per_vm: u16,
}

@ -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<u16>,
used_ports: HashSet<u16>,
}
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<u16>,
storage_pools: Vec<StoragePool>,
ipv4_pool: HashMap<String, IPData>,
ipv6_pool: HashMap<String, IPData>,
reserved_ipv4: HashSet<String>,
reserved_ipv6: HashSet<String>,
reserved_if_names: HashSet<String>,
}
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()
}
}
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 {
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<String> {
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<IPConfig>,
}
pub struct VM {
uuid: VMUUID,
uuid: String,
hostname: String,
admin_key: String,
ips: Vec<IPConfig>,
fw_ports: Vec<u16>,
nics: Vec<VMNIC>,
// 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<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 {
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.
// 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() {
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, ip.nameserver
"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(())
}

@ -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)]