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,9 +342,26 @@ 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,
|
||||
@ -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