567 lines
19 KiB
Rust
567 lines
19 KiB
Rust
#![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<u16>,
|
|
storage_pools: Vec<StoragePool>,
|
|
reserved_ips: HashSet<String>,
|
|
reserved_if_names: HashSet<String>,
|
|
// sha256sum -> absolute path
|
|
boot_files: HashMap<String, String>,
|
|
}
|
|
|
|
impl Resources {
|
|
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 {
|
|
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<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_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<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_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<String> {
|
|
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<IPConfig>,
|
|
}
|
|
|
|
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<u16>,
|
|
nics: Vec<VMNIC>,
|
|
// 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<Self, VMCreationErrors> {
|
|
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<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))
|
|
}
|