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"
|
anyhow = "1.0.94"
|
||||||
cidr = { version = "0.3.0", features = ["serde"] }
|
cidr = { version = "0.3.0", features = ["serde"] }
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
|
reqwest = { version = "0.12.9", features = ["blocking"] }
|
||||||
serde = { version = "1.0.215", features = ["derive"] }
|
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 crate::constants::*;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
use std::fs;
|
||||||
use std::fs::remove_file;
|
use std::fs::remove_file;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
use std::io::Read;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
use std::path::Path;
|
||||||
use std::process::Command;
|
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 {
|
pub struct Resources {
|
||||||
// QEMU does not support MHz limiation
|
// QEMU does not support MHz limiation
|
||||||
reserved_vcpus: usize,
|
reserved_vcpus: usize,
|
||||||
reserved_memory: usize,
|
reserved_memory: usize,
|
||||||
used_ports: HashSet<u16>,
|
reserved_ports: HashSet<u16>,
|
||||||
storage_pools: Vec<StoragePool>,
|
storage_pools: Vec<StoragePool>,
|
||||||
reserved_ipv4: HashSet<String>,
|
reserved_ips: HashSet<String>,
|
||||||
reserved_ipv6: HashSet<String>,
|
|
||||||
reserved_if_names: HashSet<String>,
|
reserved_if_names: HashSet<String>,
|
||||||
|
// sha256sum -> absolute path
|
||||||
|
boot_files: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Resources {
|
impl Resources {
|
||||||
// TODO: improve this to error out if ports are not available
|
fn reserve_ports(&mut self, nr: u16, config: &Config) -> Vec<u16> {
|
||||||
// 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> {
|
|
||||||
use rand::Rng;
|
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 {
|
if nr > config.max_ports_per_vm {
|
||||||
nr = config.max_ports_per_vm;
|
return Vec::new();
|
||||||
}
|
}
|
||||||
let mut published_ports = Vec::new();
|
let mut published_ports = Vec::new();
|
||||||
for _ in 0..nr {
|
for _ in 0..nr {
|
||||||
for _ in 0..5 {
|
for _ in 0..5 {
|
||||||
let port = rand::thread_rng().gen_range(config.public_port_range.clone());
|
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);
|
published_ports.push(port);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Vec::new()
|
published_ports
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reserve_vm_if(&mut self) -> String {
|
fn reserve_vm_if(&mut self) -> String {
|
||||||
@ -79,7 +68,7 @@ impl Resources {
|
|||||||
for range in nic.ipv4.iter() {
|
for range in nic.ipv4.iter() {
|
||||||
for ip in range.subnet.iter() {
|
for ip in range.subnet.iter() {
|
||||||
if !range.reserved_addrs.contains(&ip.address())
|
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 {
|
let if_config = match nic.driver {
|
||||||
crate::config::InterfaceType::MACVTAP => InterfaceConfig::MACVTAP {
|
crate::config::InterfaceType::MACVTAP => InterfaceConfig::MACVTAP {
|
||||||
@ -115,12 +104,13 @@ impl Resources {
|
|||||||
None
|
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> {
|
fn reserve_public_ipv6(&mut self, config: &Config) -> Option<VMNIC> {
|
||||||
for nic in config.network_interfaces.iter() {
|
for nic in config.network_interfaces.iter() {
|
||||||
for range in nic.ipv6.iter() {
|
for range in nic.ipv6.iter() {
|
||||||
for ip in range.subnet.iter() {
|
for ip in range.subnet.iter() {
|
||||||
if !range.reserved_addrs.contains(&ip.address())
|
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 {
|
let if_config = match nic.driver {
|
||||||
crate::config::InterfaceType::MACVTAP => InterfaceConfig::MACVTAP {
|
crate::config::InterfaceType::MACVTAP => InterfaceConfig::MACVTAP {
|
||||||
@ -155,6 +145,48 @@ impl Resources {
|
|||||||
}
|
}
|
||||||
None
|
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 {
|
impl InterfaceConfig {
|
||||||
@ -223,36 +255,36 @@ pub struct VM {
|
|||||||
memory: usize,
|
memory: usize,
|
||||||
// disk size in GB
|
// disk size in GB
|
||||||
disk_size: usize,
|
disk_size: usize,
|
||||||
kernel_path: String,
|
|
||||||
kernel_sha: String,
|
kernel_sha: String,
|
||||||
initrd_path: String,
|
dtrfs_sha: String,
|
||||||
initrd_sha: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct NewVMRequest {
|
pub struct NewVMRequest {
|
||||||
uuid: String,
|
uuid: String,
|
||||||
hostname: String,
|
hostname: String,
|
||||||
admin_key: String,
|
admin_key: String,
|
||||||
forwarded_ports: Vec<u16>,
|
nr_of_fw_ports: u16,
|
||||||
public_ipv4: bool,
|
public_ipv4: bool,
|
||||||
public_ipv6: bool,
|
public_ipv6: bool,
|
||||||
disk_size: usize,
|
disk_size: usize,
|
||||||
vcpus: usize,
|
vcpus: usize,
|
||||||
memory: usize,
|
memory: usize,
|
||||||
kernel_path: String,
|
kernel_url: String,
|
||||||
kernel_sha: String,
|
kernel_sha: String,
|
||||||
initrd_path: String,
|
dtrfs_url: String,
|
||||||
initrd_sha: String,
|
dtrfs_sha: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum VMCreationErrors {
|
pub enum VMCreationErrors {
|
||||||
NATandIPv4Conflict,
|
NATandIPv4Conflict,
|
||||||
TooManyCores,
|
TooManyCores,
|
||||||
|
NotEnoughPorts,
|
||||||
NotEnoughCPU,
|
NotEnoughCPU,
|
||||||
NotEnoughMemory,
|
NotEnoughMemory,
|
||||||
NotEnoughStorage,
|
NotEnoughStorage,
|
||||||
IPv4NotAvailable,
|
IPv4NotAvailable,
|
||||||
IPv6NotAvailable,
|
IPv6NotAvailable,
|
||||||
|
BootFileError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VM {
|
impl VM {
|
||||||
@ -261,7 +293,7 @@ impl VM {
|
|||||||
config: &Config,
|
config: &Config,
|
||||||
res: &mut Resources,
|
res: &mut Resources,
|
||||||
) -> Result<Self, VMCreationErrors> {
|
) -> 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);
|
return Err(VMCreationErrors::NATandIPv4Conflict);
|
||||||
}
|
}
|
||||||
if config.max_cores_per_vm < req.vcpus {
|
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) {
|
if config.max_mem_reservation < res.reserved_memory.saturating_add(req.memory) {
|
||||||
return Err(VMCreationErrors::NotEnoughMemory);
|
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();
|
let mut vm_nics = Vec::new();
|
||||||
if req.public_ipv4 {
|
if req.public_ipv4 {
|
||||||
match res.reserve_public_ipv4(config) {
|
match res.reserve_public_ipv4(config) {
|
||||||
@ -295,10 +342,27 @@ impl VM {
|
|||||||
vm_nics.push(vmnic);
|
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 {
|
Ok(VM {
|
||||||
uuid: req.uuid,
|
uuid: req.uuid,
|
||||||
hostname: req.hostname,
|
hostname: req.hostname,
|
||||||
@ -308,10 +372,8 @@ impl VM {
|
|||||||
memory: req.memory,
|
memory: req.memory,
|
||||||
disk_size: req.disk_size,
|
disk_size: req.disk_size,
|
||||||
kernel_sha: req.kernel_sha,
|
kernel_sha: req.kernel_sha,
|
||||||
initrd_sha: req.initrd_sha,
|
dtrfs_sha: req.dtrfs_sha,
|
||||||
fw_ports: todo!(),
|
fw_ports,
|
||||||
kernel_path: todo!(),
|
|
||||||
initrd_path: todo!(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -371,8 +433,8 @@ impl VM {
|
|||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
vars += &format!("KERNEL={}\n", self.kernel_path);
|
vars += &format!("KERNEL={}\n", VM_BOOT_DIR.to_string() + "/" + &self.kernel_sha);
|
||||||
vars += &format!("INITRD={}\n", self.initrd_path);
|
vars += &format!("INITRD={}\n", VM_BOOT_DIR.to_string() + "/" + &self.dtrfs_sha);
|
||||||
vars += &format!("PARAMS={}\n", self.kernel_params());
|
vars += &format!("PARAMS={}\n", self.kernel_params());
|
||||||
vars += &format!("CPU_TYPE={}\n", QEMU_VM_CPU_TYPE);
|
vars += &format!("CPU_TYPE={}\n", QEMU_VM_CPU_TYPE);
|
||||||
vars += &format!("VCPUS={}\n", self.vcpus);
|
vars += &format!("VCPUS={}\n", self.vcpus);
|
||||||
@ -460,3 +522,45 @@ impl VM {
|
|||||||
Ok(())
|
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 alloc: ResourceAllocation,
|
||||||
pub kernel_uri: String,
|
pub kernel_uri: String,
|
||||||
pub kernel_sha: String,
|
pub kernel_sha: String,
|
||||||
pub initrd_uri: String,
|
pub dtrfs_uri: String,
|
||||||
pub initrd_sha: String,
|
pub dtrfs_sha: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
Loading…
Reference in New Issue
Block a user