added multiple new capabilities for daemon

- systemctl integration (reload, start, stop, enable, disable)
- update vm capability
- delete vm capability
- save resources to disk
- save VMs to disk
- load commands from folders (new vm, update vm, delete vm)
- fixed port forwarding
This commit is contained in:
ghe0 2024-12-14 01:10:30 +02:00
parent 82273f63e5
commit b85feef0ab
Signed by: ghe0
GPG Key ID: 451028EE56A0FBB4
9 changed files with 404 additions and 82 deletions

@ -0,0 +1,13 @@
uuid: "uuid-0001"
hostname: "ghe-vm-1"
admin_key: "MCowBQYDK2VwAyEAEoJ50VwJc7noWxylhioU2kk35MkO5as4U92UbP2A7xk="
extra_ports: [80]
public_ipv4: false
public_ipv6: true
disk_size_gb: 20
vcpus: 2
memory_mb: 2000
kernel_url: "https://drive.google.com/uc?export=download&id=1bc2CmJjIBFSXRxTFQJy11uSobsazTs4n"
kernel_sha: "203352667d403b437c856bec16809604e801e4f0cfabbf938daa8fda2fad0f84"
dtrfs_url: "https://drive.google.com/uc?export=download&id=1WoAVb9VS0rlzmuEIwwzZMx7N7SsUQf6q"
dtrfs_sha: "9fe9ae795a239a426a290f21dfa857182e3fe2fa556f8e8997ab35dfeb8fca24"

@ -1,5 +1,5 @@
uuid: "0000000000000000000-0001" uuid: "uuid-0002"
hostname: "ghe-vm-1" hostname: "ghe-vm-2"
admin_key: "MCowBQYDK2VwAyEAEoJ50VwJc7noWxylhioU2kk35MkO5as4U92UbP2A7xk=" admin_key: "MCowBQYDK2VwAyEAEoJ50VwJc7noWxylhioU2kk35MkO5as4U92UbP2A7xk="
extra_ports: [] extra_ports: []
public_ipv4: true public_ipv4: true
@ -9,5 +9,5 @@ vcpus: 2
memory_mb: 2000 memory_mb: 2000
kernel_url: "https://drive.google.com/uc?export=download&id=1bc2CmJjIBFSXRxTFQJy11uSobsazTs4n" kernel_url: "https://drive.google.com/uc?export=download&id=1bc2CmJjIBFSXRxTFQJy11uSobsazTs4n"
kernel_sha: "203352667d403b437c856bec16809604e801e4f0cfabbf938daa8fda2fad0f84" kernel_sha: "203352667d403b437c856bec16809604e801e4f0cfabbf938daa8fda2fad0f84"
dtrfs_url: "https://drive.google.com/uc?export=download&id=1r44_g-mi1PlFu5lmhN1kbuZdJgATINsg" dtrfs_url: "https://drive.google.com/uc?export=download&id=1WoAVb9VS0rlzmuEIwwzZMx7N7SsUQf6q"
dtrfs_sha: "f347574bb637c306aaf0f8d0650efc90b95cdc61ea6c1c0f5b87b7d162aae4f3" dtrfs_sha: "9fe9ae795a239a426a290f21dfa857182e3fe2fa556f8e8997ab35dfeb8fca24"

@ -0,0 +1,13 @@
uuid: "uuid-0004"
hostname: "ghe-vm-4"
admin_key: "MCowBQYDK2VwAyEAEoJ50VwJc7noWxylhioU2kk35MkO5as4U92UbP2A7xk="
extra_ports: []
public_ipv4: false
public_ipv6: false
disk_size_gb: 20
vcpus: 2
memory_mb: 2000
kernel_url: "https://drive.google.com/uc?export=download&id=1bc2CmJjIBFSXRxTFQJy11uSobsazTs4n"
kernel_sha: "203352667d403b437c856bec16809604e801e4f0cfabbf938daa8fda2fad0f84"
dtrfs_url: "https://drive.google.com/uc?export=download&id=1WoAVb9VS0rlzmuEIwwzZMx7N7SsUQf6q"
dtrfs_sha: "9fe9ae795a239a426a290f21dfa857182e3fe2fa556f8e8997ab35dfeb8fca24"

@ -0,0 +1,13 @@
uuid: "uuid-0003"
hostname: "ghe-vm-3"
admin_key: "MCowBQYDK2VwAyEAEoJ50VwJc7noWxylhioU2kk35MkO5as4U92UbP2A7xk="
extra_ports: []
public_ipv4: true
public_ipv6: false
disk_size_gb: 20
vcpus: 2
memory_mb: 2000
kernel_url: "https://drive.google.com/uc?export=download&id=1bc2CmJjIBFSXRxTFQJy11uSobsazTs4n"
kernel_sha: "203352667d403b437c856bec16809604e801e4f0cfabbf938daa8fda2fad0f84"
dtrfs_url: "https://drive.google.com/uc?export=download&id=1WoAVb9VS0rlzmuEIwwzZMx7N7SsUQf6q"
dtrfs_sha: "9fe9ae795a239a426a290f21dfa857182e3fe2fa556f8e8997ab35dfeb8fca24"

@ -4,7 +4,7 @@
echo "Environment variable VM_UUID is not set." echo "Environment variable VM_UUID is not set."
exit 1 exit 1
} }
source "/etc/detee/daemon/vms/$VM_UUID" source "/etc/detee/daemon/vms/${VM_UUID}.sh"
mandatory_vars=("KERNEL" "INITRD" "PARAMS" "CPU_TYPE" \ mandatory_vars=("KERNEL" "INITRD" "PARAMS" "CPU_TYPE" \
"VCPUS" "MEMORY" "MAX_MEMORY" "DISK") "VCPUS" "MEMORY" "MAX_MEMORY" "DISK")
@ -15,9 +15,10 @@ for var in "${mandatory_vars[@]}"; do
fi fi
done done
interfaces=$(env | grep -oE '^NETWORK_INTERFACE_[0-9]*') interfaces=$(env | sort | grep -oE '^NETWORK_INTERFACE_[0-9]*')
nat_configured="false" nat_configured="false"
vtap_fd_counter=3 vtap_nic_count=1
qemu_device_params=""
while read -r interface; do while read -r interface; do
interface_type="$( echo ${!interface} | cut -d '_' -f1 )" interface_type="$( echo ${!interface} | cut -d '_' -f1 )"
@ -28,38 +29,37 @@ while read -r interface; do
if [[ "$interface_type" == "macvtap" ]]; then if [[ "$interface_type" == "macvtap" ]]; then
ip link add link $interface_device name $interface_name type $interface_type mode bridge ip link add link $interface_device name $interface_name type $interface_type mode bridge
else else
ip link add link $interface_device name $interface_name type $interface_type ip link add link $interface_device name $interface_name type $interface_type mode l3
fi fi
sysctl -w net.ipv6.conf.$interface_name.accept_ra=0
ip link set $interface_name up ip link set $interface_name up
ip link set $interface_name promisc on ip link set $interface_name promisc on
vtap_index="$(cat /sys/class/net/${interface_name}/ifindex)" vtap_index="$(cat /sys/class/net/${interface_name}/ifindex)"
vtap_addr="$(cat /sys/class/net/${interface_name}/address)" vtap_addr="$(cat /sys/class/net/${interface_name}/address)"
fd_number=$vtap_fd_counter
exec {fd_number}<> /dev/tap${vtap_index} exec {fd_number}<> /dev/tap${vtap_index}
qemu_device_params="-netdev tap,id=hostnet1,fd=${fd_number} -device virtio-net-pci,netdev=hostnet1,mac=${vtap_addr},romfile=" qemu_device_params+=" -netdev tap,id=hostnet1,fd=${fd_number}"
((vtap_fd_counter++)) qemu_device_params+=" -device virtio-net-pci,netdev=hostnet${vtap_nic_count},mac=${vtap_addr},romfile="
((vtap_nic_count++))
fi fi
if [[ "$interface_type" == "NAT" && "$nat_configured" == "false" ]]; then if [[ "$interface_type" == "NAT" && "$nat_configured" == "false" ]]; then
ports="" ports=""
nat_configured="true" nat_configured="true"
for port_pair in "$NAT_PORT_FW"; do for port_pair in $NAT_PORT_FW; do
host_port="$( echo $port_pair | cut -d ':' -f1 )" host_port="$( echo $port_pair | cut -d ':' -f1 )"
guest_port="$( echo $port_pair | cut -d ':' -f2 )" guest_port="$( echo $port_pair | cut -d ':' -f2 )"
ports+=",hostfwd=tcp::${host_port}-:${guest_port}" ports+=",hostfwd=tcp::${host_port}-:${guest_port}"
done done
qemu_device_params="-netdev user,id=vmnic${ports}" qemu_device_params+=" -netdev user,id=natnic${ports}"
qemu_device_params+=" -device virtio-net-pci,netdev=vmnic,romfile=" qemu_device_params+=" -device virtio-net-pci,netdev=natnic,romfile="
fi fi
# TODO: also handle bridge device (when IPs are public, but the host is the gateway) # TODO: also handle bridge device (when IPs are public, but the host is the gateway)
done <<< "$( echo "$interfaces" )" done <<< "$( echo "$interfaces" )"
vm_disk="/root/dtrfs/arch-1-ghe0.qcow2"
qemu-system-x86_64 $qemu_device_params \ qemu-system-x86_64 $qemu_device_params \
-enable-kvm -cpu $CPU_TYPE -vga none \ -enable-kvm -cpu $CPU_TYPE -vga none \
-machine q35,confidential-guest-support=sev0,memory-backend=ram1 \ -machine q35,confidential-guest-support=sev0,memory-backend=ram1 \

@ -5,7 +5,7 @@ use std::collections::HashSet;
use std::net::{Ipv4Addr, Ipv6Addr}; use std::net::{Ipv4Addr, Ipv6Addr};
use std::ops::Range; use std::ops::Range;
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug, Clone)]
pub struct Volume { pub struct Volume {
pub path: String, pub path: String,
pub max_reservation_gb: usize, pub max_reservation_gb: usize,
@ -83,7 +83,7 @@ mod range_format {
} }
impl Config { impl Config {
pub fn from_file(path: &str) -> Result<Self, Box<dyn std::error::Error>> { pub fn load_from_disk(path: &str) -> Result<Self, Box<dyn std::error::Error>> {
let content = std::fs::read_to_string(path)?; let content = std::fs::read_to_string(path)?;
let config: Config = serde_yaml::from_str(&content)?; let config: Config = serde_yaml::from_str(&content)?;
Ok(config) Ok(config)

@ -2,6 +2,7 @@
pub(crate) const DEFAULT_OVMF: &str = "/usr/share/edk2/ovmf/OVMF.amdsev.fd"; pub(crate) const DEFAULT_OVMF: &str = "/usr/share/edk2/ovmf/OVMF.amdsev.fd";
pub(crate) const VM_BOOT_DIR: &str = "/var/lib/detee/boot/"; pub(crate) const VM_BOOT_DIR: &str = "/var/lib/detee/boot/";
pub(crate) const USED_RESOURCES: &str = "/etc/detee/daemon/used_resources.yaml";
pub(crate) const VM_CONFIG_DIR: &str = "/etc/detee/daemon/vms/"; pub(crate) const VM_CONFIG_DIR: &str = "/etc/detee/daemon/vms/";
pub(crate) const DAEMON_CONFIG_PATH: &str = "/etc/detee/daemon/config.yaml"; pub(crate) const DAEMON_CONFIG_PATH: &str = "/etc/detee/daemon/config.yaml";
pub(crate) const START_VM_SCRIPT: &str = "/usr/local/bin/detee/start_qemu_vm.sh"; pub(crate) const START_VM_SCRIPT: &str = "/usr/local/bin/detee/start_qemu_vm.sh";

@ -1,18 +1,60 @@
mod config; mod config;
mod state;
mod constants; mod constants;
mod state;
mod tcontract; mod tcontract;
use crate::config::Config; use crate::config::Config;
use crate::state::NewVMRequest; use crate::state::NewVMRequest;
use crate::state::UpdateVMReq;
use std::fs::read_dir;
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = std::env::args().collect(); let args: Vec<String> = std::env::args().collect();
let config = Config::from_file(crate::constants::DAEMON_CONFIG_PATH)?; let config = Config::load_from_disk(crate::constants::DAEMON_CONFIG_PATH)?;
let mut res = state::Resources::new(&config.volumes); let mut res = match state::Resources::load_from_disk() {
let new_vm_req = NewVMRequest::from_file(&args[1])?; Ok(res) => res,
let vm = state::VM::new(new_vm_req, &config, &mut res); Err(e) => {
println!("Got VM: {:#?}", vm); println!("Could not load resources from disk: {e:?}");
println!("Starting VM... \n{:?}", vm.unwrap().start()); println!("Creating new resource calculator.");
state::Resources::new(&config.volumes)
}
};
for entry in read_dir("/etc/detee/daemon/newvmreq/")? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
let new_vm_req = NewVMRequest::from_file(path.to_str().unwrap())?;
let vm = state::VM::new(new_vm_req, &config, &mut res).unwrap();
vm.start()?;
println!("started vm {}", vm.uuid);
}
}
for entry in read_dir("/etc/detee/daemon/deletevm/")? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
let vm_id = path.file_name().unwrap().to_str().unwrap();
let content = std::fs::read_to_string(crate::constants::VM_CONFIG_DIR.to_string() + vm_id + ".yaml")?;
let vm: crate::state::VM = serde_yaml::from_str(&content)?;
vm.delete(&mut res)?;
println!("deleted vm {}", vm.uuid);
}
}
for entry in read_dir("/etc/detee/daemon/updatedvmreq/")? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
let new_vm_req = UpdateVMReq::from_file(&args[1])?;
let content = std::fs::read_to_string(crate::constants::VM_CONFIG_DIR.to_string() + &new_vm_req.uuid + ".yaml")?;
let mut vm: crate::state::VM = serde_yaml::from_str(&content)?;
vm.update(new_vm_req, &config, &mut res).unwrap();
println!("updated vm {}", vm.uuid);
}
}
Ok(()) Ok(())
} }

@ -4,7 +4,9 @@ use crate::constants::*;
use anyhow::anyhow; use anyhow::anyhow;
use anyhow::Result; use anyhow::Result;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::collections::HashSet; use std::collections::HashSet;
use std::fs; use std::fs;
use std::fs::remove_file; use std::fs::remove_file;
@ -15,13 +17,14 @@ use std::net::{Ipv4Addr, Ipv6Addr};
use std::path::Path; use std::path::Path;
use std::process::Command; use std::process::Command;
#[derive(Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct Resources { pub struct Resources {
existing_vms: HashSet<String>,
// QEMU does not support MHz limiation // QEMU does not support MHz limiation
reserved_vcpus: usize, reserved_vcpus: usize,
reserved_memory: usize, reserved_memory: usize,
reserved_ports: HashSet<u16>, reserved_ports: HashSet<u16>,
storage_pools: Vec<StoragePool>, reserved_storage: HashMap<String, usize>,
reserved_ips: HashSet<String>, reserved_ips: HashSet<String>,
reserved_if_names: HashSet<String>, reserved_if_names: HashSet<String>,
// sha256sum -> absolute path // sha256sum -> absolute path
@ -29,6 +32,19 @@ pub struct Resources {
} }
impl Resources { impl Resources {
fn save_to_disk(&self) -> Result<()> {
let mut file = File::create(USED_RESOURCES)?;
file.write_all(serde_yaml::to_string(self)?.as_bytes())?;
Ok(())
}
pub fn load_from_disk() -> Result<Self> {
let content = std::fs::read_to_string(USED_RESOURCES)?;
let mut res: Self = serde_yaml::from_str(&content)?;
res.scan_boot_files().unwrap();
Ok(res)
}
pub fn new(config_volumes: &Vec<crate::config::Volume>) -> Self { pub fn new(config_volumes: &Vec<crate::config::Volume>) -> Self {
let mut storage_pools = Vec::new(); let mut storage_pools = Vec::new();
for config_vol in config_volumes.iter() { for config_vol in config_volumes.iter() {
@ -39,23 +55,32 @@ impl Resources {
}); });
} }
let mut res = Resources { let mut res = Resources {
existing_vms: HashSet::new(),
reserved_vcpus: 0, reserved_vcpus: 0,
reserved_memory: 0, reserved_memory: 0,
reserved_ports: HashSet::new(), reserved_ports: HashSet::new(),
storage_pools, reserved_storage: HashMap::new(),
reserved_ips: HashSet::new(), reserved_ips: HashSet::new(),
reserved_if_names: HashSet::new(), reserved_if_names: HashSet::new(),
boot_files: HashSet::new(), boot_files: HashSet::new(),
}; };
res.scan_boot_files().unwrap(); res.scan_boot_files().unwrap();
let _ = res.save_to_disk();
res res
} }
fn available_storage_pool(&mut self, required_gb: usize) -> Option<String> { fn available_storage_pool(&mut self, required_gb: usize, config: &Config) -> Option<String> {
self.storage_pools.sort_by_key(|p| p.available_gb); let mut volumes = config.volumes.clone();
let pool = self.storage_pools.last()?; for volume in volumes.iter_mut() {
if pool.available_gb > required_gb { if let Some(reservation) = self.reserved_storage.get(&volume.path) {
return Some(pool.path.clone()); volume.max_reservation_gb -= reservation;
}
}
volumes.sort_by_key(|v| v.max_reservation_gb);
if let Some(biggest_volume) = volumes.last() {
if biggest_volume.max_reservation_gb > required_gb {
return Some(biggest_volume.path.clone());
}
} }
None None
} }
@ -133,7 +158,6 @@ impl Resources {
None None
} }
// TODO: refactor this garbage cause it's only one char different from the previous one
fn available_ipv6(&mut self, config: &Config) -> Option<VMNIC> { fn available_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() {
@ -198,6 +222,7 @@ impl Resources {
} }
fn reserve_vm_resources(&mut self, vm: &VM) { fn reserve_vm_resources(&mut self, vm: &VM) {
self.existing_vms.insert(vm.uuid.clone());
self.reserved_vcpus += vm.vcpus; self.reserved_vcpus += vm.vcpus;
self.reserved_memory += vm.memory_mb; self.reserved_memory += vm.memory_mb;
for nic in vm.nics.iter() { for nic in vm.nics.iter() {
@ -212,15 +237,15 @@ impl Resources {
self.reserved_ports.insert(*host_port); self.reserved_ports.insert(*host_port);
} }
for storage_pool in self.storage_pools.iter_mut() { self.reserved_storage
if storage_pool.path == vm.storage_pool_path { .entry(vm.storage_dir.clone())
storage_pool.available_gb -= vm.disk_size_gb; .and_modify(|gb| *gb += vm.disk_size_gb)
break; .or_insert(vm.disk_size_gb);
} let _ = self.save_to_disk();
}
} }
fn free_vm_resources(&mut self, vm: &VM) { fn free_vm_resources(&mut self, vm: &VM) {
self.existing_vms.remove(&vm.uuid);
self.reserved_vcpus -= vm.vcpus; self.reserved_vcpus -= vm.vcpus;
self.reserved_memory -= vm.memory_mb; self.reserved_memory -= vm.memory_mb;
for nic in vm.nics.iter() { for nic in vm.nics.iter() {
@ -234,10 +259,14 @@ impl Resources {
for (host_port, _) in vm.fw_ports.iter() { for (host_port, _) in vm.fw_ports.iter() {
self.reserved_ports.remove(host_port); self.reserved_ports.remove(host_port);
} }
self.reserved_storage
.entry(vm.storage_dir.clone())
.and_modify(|gb| *gb -= vm.disk_size_gb);
let _ = self.save_to_disk();
} }
} }
#[derive(Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct StoragePool { pub struct StoragePool {
path: String, path: String,
available_gb: usize, available_gb: usize,
@ -245,12 +274,11 @@ pub struct StoragePool {
// tier: StorageTier, // tier: StorageTier,
} }
#[derive(Debug)] #[derive(Serialize, Deserialize, Debug)]
pub enum InterfaceConfig { pub enum InterfaceConfig {
// 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 },
// TODO: figure how to calculate IF_NAME based on index
MACVTAP { name: String, device: String }, MACVTAP { name: String, device: String },
IPVTAP { name: String, device: String }, IPVTAP { name: String, device: String },
Bridge { device: String }, Bridge { device: String },
@ -291,7 +319,7 @@ impl InterfaceConfig {
} }
} }
#[derive(Debug)] #[derive(Serialize, Deserialize, Debug)]
struct IPConfig { struct IPConfig {
address: String, address: String,
// requires short format (example: 24) // requires short format (example: 24)
@ -299,7 +327,7 @@ struct IPConfig {
gateway: String, gateway: String,
} }
#[derive(Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct VMNIC { pub struct VMNIC {
if_config: InterfaceConfig, if_config: InterfaceConfig,
ips: Vec<IPConfig>, ips: Vec<IPConfig>,
@ -311,9 +339,9 @@ impl VMNIC {
} }
} }
#[derive(Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct VM { pub struct VM {
uuid: String, pub uuid: String,
hostname: String, hostname: String,
admin_key: String, admin_key: String,
fw_ports: Vec<(u16, u16)>, fw_ports: Vec<(u16, u16)>,
@ -325,7 +353,7 @@ pub struct VM {
disk_size_gb: usize, disk_size_gb: usize,
kernel_sha: String, kernel_sha: String,
dtrfs_sha: String, dtrfs_sha: String,
storage_pool_path: String, storage_dir: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@ -353,8 +381,25 @@ impl NewVMRequest {
} }
} }
#[derive(Deserialize, Debug)]
pub struct UpdateVMReq {
pub uuid: String,
vcpus: usize,
memory_mb: usize,
disk_size_gb: usize,
}
impl UpdateVMReq {
pub fn from_file(path: &str) -> Result<Self, Box<dyn std::error::Error>> {
let content = std::fs::read_to_string(path)?;
let request: UpdateVMReq = serde_yaml::from_str(&content)?;
Ok(request)
}
}
#[derive(Debug)] #[derive(Debug)]
pub enum VMCreationErrors { pub enum VMCreationErrors {
VMAlreadyExists,
NATandIPv4Conflict, NATandIPv4Conflict,
TooManyCores, TooManyCores,
NotEnoughPorts, NotEnoughPorts,
@ -363,7 +408,10 @@ pub enum VMCreationErrors {
NotEnoughStorage, NotEnoughStorage,
IPv4NotAvailable, IPv4NotAvailable,
IPv6NotAvailable, IPv6NotAvailable,
DiskTooSmall,
ServerDiskError(String),
BootFileError(String), BootFileError(String),
HypervizorError(String),
} }
impl VM { impl VM {
@ -372,6 +420,9 @@ impl VM {
config: &Config, config: &Config,
res: &mut Resources, res: &mut Resources,
) -> Result<Self, VMCreationErrors> { ) -> Result<Self, VMCreationErrors> {
if res.existing_vms.contains(&req.uuid) {
return Err(VMCreationErrors::VMAlreadyExists);
}
if req.extra_ports.len() > 0 && req.public_ipv4 { if req.extra_ports.len() > 0 && req.public_ipv4 {
return Err(VMCreationErrors::NATandIPv4Conflict); return Err(VMCreationErrors::NATandIPv4Conflict);
} }
@ -384,6 +435,9 @@ impl VM {
if config.max_mem_reservation_mb < res.reserved_memory.saturating_add(req.memory_mb) { if config.max_mem_reservation_mb < res.reserved_memory.saturating_add(req.memory_mb) {
return Err(VMCreationErrors::NotEnoughMemory); return Err(VMCreationErrors::NotEnoughMemory);
} }
if req.disk_size_gb < 4 {
return Err(VMCreationErrors::DiskTooSmall);
}
if let Err(kernel_file_error) = if let Err(kernel_file_error) =
res.download_boot_file(req.kernel_url, req.kernel_sha.clone()) res.download_boot_file(req.kernel_url, req.kernel_sha.clone())
@ -440,7 +494,7 @@ impl VM {
} }
} }
let storage_pool_path = match res.available_storage_pool(req.disk_size_gb) { let storage_pool_path = match res.available_storage_pool(req.disk_size_gb, config) {
Some(path) => path, Some(path) => path,
None => return Err(VMCreationErrors::NotEnoughStorage), None => return Err(VMCreationErrors::NotEnoughStorage),
}; };
@ -456,40 +510,103 @@ impl VM {
kernel_sha: req.kernel_sha, kernel_sha: req.kernel_sha,
dtrfs_sha: req.dtrfs_sha, dtrfs_sha: req.dtrfs_sha,
fw_ports: port_pairs, fw_ports: port_pairs,
storage_pool_path, storage_dir: storage_pool_path,
}; };
if let Err(e) = vm.write_config() {
return Err(VMCreationErrors::ServerDiskError(e.to_string()));
}
res.reserve_vm_resources(&vm); res.reserve_vm_resources(&vm);
Ok(vm) Ok(vm)
} }
// TODO: test to see if this works
pub fn update(
&mut self,
req: UpdateVMReq,
config: &Config,
res: &mut Resources,
) -> Result<(), VMCreationErrors> {
if config.max_cores_per_vm < req.vcpus {
return Err(VMCreationErrors::TooManyCores);
}
if config.max_vcpu_reservation
< res
.reserved_vcpus
.saturating_sub(self.vcpus)
.saturating_add(req.vcpus)
{
return Err(VMCreationErrors::NotEnoughCPU);
}
if config.max_mem_reservation_mb
< res
.reserved_memory
.saturating_sub(self.memory_mb)
.saturating_add(req.memory_mb)
{
return Err(VMCreationErrors::NotEnoughMemory);
}
if req.disk_size_gb < self.disk_size_gb {
return Err(VMCreationErrors::DiskTooSmall);
}
res.reserved_memory -= self.memory_mb;
res.reserved_memory += req.memory_mb;
res.reserved_vcpus -= self.vcpus;
res.reserved_vcpus += req.vcpus;
res.reserved_storage
.entry(self.storage_dir.clone())
.and_modify(|gb| {
*gb -= self.disk_size_gb;
*gb += req.disk_size_gb;
});
self.memory_mb = req.memory_mb;
self.vcpus = req.vcpus;
self.disk_size_gb = req.disk_size_gb;
if let Err(e) = systemctl_stop_and_disable(&self.uuid) {
return Err(VMCreationErrors::HypervizorError(e.to_string()));
}
if let Err(e) = self.write_config() {
return Err(VMCreationErrors::ServerDiskError(e.to_string()));
}
if let Err(e) = self.write_sh_exports() {
return Err(VMCreationErrors::ServerDiskError(e.to_string()));
}
if let Err(e) = self.resize_disk() {
return Err(VMCreationErrors::HypervizorError(e.to_string()));
}
if let Err(e) = systemctl_stop_and_disable(&self.uuid) {
return Err(VMCreationErrors::HypervizorError(e.to_string()));
}
Ok(())
}
pub fn start(&self) -> Result<()> { pub fn start(&self) -> Result<()> {
self.create_disk()?; self.create_disk()?;
self.write_vm_config()?; self.write_sh_exports()?;
self.write_systemd_unit_file()?; self.write_systemd_unit_file()?;
// TODO: systemctl daemon-reload systemctl_reload()?;
// TODO: systemctl start systemctl_start_and_enable(&self.uuid)?;
// TODO: systemctl enable
Ok(()) Ok(())
} }
pub fn delete(&self, res: &mut Resources) -> Result<()> { pub fn delete(&self, res: &mut Resources) -> Result<()> {
// TODO: systemctl stop systemctl_stop_and_disable(&self.uuid)?;
// TODO: systemctl disable
res.free_vm_resources(&self); res.free_vm_resources(&self);
self.delete_systemd_unit_file()?; self.delete_systemd_unit_file()?;
// TODO: systemctl daemon-reload systemctl_reload()?;
self.delete_disk()?; self.delete_disk()?;
self.delete_vm_config()?; self.delete_sh_exports()?;
self.delete_vtap_interfaces()?; self.delete_vtap_interfaces()?;
Ok(()) self.delete_config()?;
}
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(()) Ok(())
} }
@ -497,9 +614,9 @@ impl VM {
// 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 {
let dir = match self.storage_pool_path.ends_with("/") { let dir = match self.storage_dir.ends_with("/") {
true => self.storage_pool_path.clone(), true => self.storage_dir.clone(),
false => self.storage_pool_path.clone() + "/", false => self.storage_dir.clone() + "/",
}; };
dir + &self.uuid + ".qcow2" dir + &self.uuid + ".qcow2"
} }
@ -507,6 +624,13 @@ impl VM {
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;
if self.fw_ports.len() > 0 {
ip_string += &format!(
"detee_net_eth{}={}_{}_{} ",
i, "10.0.2.15", "24", "10.0.2.2"
);
i += 1;
}
for nic in self.nics.iter() { for nic in self.nics.iter() {
for ip in nic.ips.iter() { for ip in nic.ips.iter() {
ip_string += &format!( ip_string += &format!(
@ -521,13 +645,28 @@ impl VM {
format!("{} {} {}", ip_string, admin_key, hostname) format!("{} {} {}", ip_string, admin_key, hostname)
} }
pub fn write_vm_config(&self) -> Result<()> { fn write_config(&self) -> Result<()> {
let mut file = File::create(VM_CONFIG_DIR.to_string() + &self.uuid + ".yaml")?;
file.write_all(serde_yaml::to_string(self)?.as_bytes())?;
Ok(())
}
fn delete_config(&self) -> Result<()> {
remove_file(VM_CONFIG_DIR.to_string() + &self.uuid + ".yaml")?;
Ok(())
}
fn write_sh_exports(&self) -> Result<()> {
let mut vars = String::new(); let mut vars = String::new();
let mut i = 0; let mut i = 0;
for nic in self.nics.iter() { for nic in self.nics.iter() {
let mut interface = String::new(); let mut interface = String::new();
interface += &format!(r#"export NETWORK_INTERFACE_{}="{}"#, i, nic.if_config.if_type()); interface += &format!(
r#"export 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) = nic.if_config.vtap_name() { if let Some(vtap_name) = nic.if_config.vtap_name() {
interface += &format!("_{}_{}", nic.if_config.device_name(), vtap_name); interface += &format!("_{}_{}", nic.if_config.device_name(), vtap_name);
@ -543,8 +682,9 @@ impl VM {
ports += &format!("{}:{} ", port.0, port.1); ports += &format!("{}:{} ", port.0, port.1);
} }
if ports != "" { if ports != "" {
vars += &format!(r#"NAT_PORT_FW="{}""#, ports.trim_end()); vars += &format!(r#"export NAT_PORT_FW="{}""#, ports.trim_end());
vars += "\n"; vars += "\n";
vars += "export NETWORK_INTERFACE_0000=NAT\n";
} }
vars += &format!( vars += &format!(
@ -570,17 +710,17 @@ impl VM {
vars += &format!(r#"export DISK="{}""#, self.disk_path()); vars += &format!(r#"export DISK="{}""#, self.disk_path());
vars += "\n"; vars += "\n";
let mut file = File::create(VM_CONFIG_DIR.to_string() + &self.uuid)?; let mut file = File::create(VM_CONFIG_DIR.to_string() + &self.uuid + ".sh")?;
file.write_all(vars.as_bytes())?; file.write_all(vars.as_bytes())?;
Ok(()) Ok(())
} }
pub fn delete_vm_config(&self) -> Result<()> { fn delete_sh_exports(&self) -> Result<()> {
remove_file(VM_CONFIG_DIR.to_string() + &self.uuid)?; remove_file(VM_CONFIG_DIR.to_string() + &self.uuid + ".sh")?;
Ok(()) Ok(())
} }
pub fn delete_vtap_interfaces(&self) -> Result<()> { fn delete_vtap_interfaces(&self) -> Result<()> {
for nic in self.nics.iter() { for nic in self.nics.iter() {
if let Some(name) = nic.if_config.vtap_name() { if let Some(name) = nic.if_config.vtap_name() {
let result = Command::new("ip") let result = Command::new("ip")
@ -601,7 +741,7 @@ impl VM {
Ok(()) Ok(())
} }
pub fn write_systemd_unit_file(&self) -> Result<()> { fn write_systemd_unit_file(&self) -> Result<()> {
let mut contents = String::new(); let mut contents = String::new();
contents += &format!("[Unit]\n"); contents += &format!("[Unit]\n");
contents += &format!("Description=DeTEE {}\n", self.uuid); contents += &format!("Description=DeTEE {}\n", self.uuid);
@ -617,17 +757,25 @@ impl VM {
contents += &format!("[Install]\n"); contents += &format!("[Install]\n");
contents += &format!("WantedBy=multi-user.target\n"); contents += &format!("WantedBy=multi-user.target\n");
let mut file = File::create("/etc/systemd/system/".to_string() + &self.uuid + ".service")?; let mut file =
File::create_new("/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<()> { fn delete_systemd_unit_file(&self) -> Result<()> {
remove_file("/tmp/etc/systemd/system/".to_string() + &self.uuid + ".service")?; remove_file("/etc/systemd/system/".to_string() + &self.uuid + ".service")?;
Ok(()) Ok(())
} }
pub fn create_disk(&self) -> Result<()> { fn create_disk(&self) -> Result<()> {
if std::path::Path::new(&self.disk_path()).exists() {
return Err(anyhow!(
"Could not create {}. The file already exists.",
self.disk_path()
));
}
let result = Command::new("qemu-img") let result = Command::new("qemu-img")
.arg("create") .arg("create")
.arg("-f") .arg("-f")
@ -647,12 +795,104 @@ impl VM {
Ok(()) Ok(())
} }
pub fn delete_disk(&self) -> Result<()> { fn resize_disk(&self) -> Result<()> {
let result = Command::new("qemu-img")
.arg("resize")
.arg(self.disk_path())
.arg(self.disk_size_gb.to_string() + "G")
.output()?;
if !result.status.success() {
return Err(anyhow!(
"Could not create VM Disk:\n{:?}\n{:?}",
String::from_utf8(result.stdout)
.unwrap_or("Could not grab stdout from creation script.".to_string()),
String::from_utf8(result.stderr)
.unwrap_or("Could not grab stderr from creation script.".to_string()),
));
}
Ok(())
}
fn delete_disk(&self) -> Result<()> {
remove_file(self.disk_path())?; remove_file(self.disk_path())?;
Ok(()) Ok(())
} }
} }
fn systemctl_start_and_enable(vm_uuid: &str) -> Result<()> {
let result = Command::new("systemctl")
.arg("start")
.arg(vm_uuid.to_string() + ".service")
.output()?;
if !result.status.success() {
return Err(anyhow!(
"Could not reload systemctl daemon:\n{:?}\n{:?}",
String::from_utf8(result.stdout)
.unwrap_or("Could not grab stdout from creation script.".to_string()),
String::from_utf8(result.stderr)
.unwrap_or("Could not grab stderr from creation script.".to_string()),
));
}
let result = Command::new("systemctl")
.arg("enable")
.arg(vm_uuid.to_string() + ".service")
.output()?;
if !result.status.success() {
return Err(anyhow!(
"Could not reload systemctl daemon:\n{:?}\n{:?}",
String::from_utf8(result.stdout)
.unwrap_or("Could not grab stdout from creation script.".to_string()),
String::from_utf8(result.stderr)
.unwrap_or("Could not grab stderr from creation script.".to_string()),
));
}
Ok(())
}
fn systemctl_stop_and_disable(vm_uuid: &str) -> Result<()> {
let result = Command::new("systemctl")
.arg("stop")
.arg(vm_uuid.to_string() + ".service")
.output()?;
if !result.status.success() {
return Err(anyhow!(
"Could not reload systemctl daemon:\n{:?}\n{:?}",
String::from_utf8(result.stdout)
.unwrap_or("Could not grab stdout from creation script.".to_string()),
String::from_utf8(result.stderr)
.unwrap_or("Could not grab stderr from creation script.".to_string()),
));
}
let result = Command::new("systemctl")
.arg("disable")
.arg(vm_uuid.to_string() + ".service")
.output()?;
if !result.status.success() {
return Err(anyhow!(
"Could not reload systemctl daemon:\n{:?}\n{:?}",
String::from_utf8(result.stdout)
.unwrap_or("Could not grab stdout from creation script.".to_string()),
String::from_utf8(result.stderr)
.unwrap_or("Could not grab stderr from creation script.".to_string()),
));
}
Ok(())
}
fn systemctl_reload() -> Result<()> {
let result = Command::new("systemctl").arg("daemon-reload").output()?;
if !result.status.success() {
return Err(anyhow!(
"Could not reload systemctl daemon:\n{:?}\n{:?}",
String::from_utf8(result.stdout)
.unwrap_or("Could not grab stdout from creation script.".to_string()),
String::from_utf8(result.stderr)
.unwrap_or("Could not grab stderr from creation script.".to_string()),
));
}
Ok(())
}
fn download_and_check_sha(url: &str, sha: &str) -> Result<()> { fn download_and_check_sha(url: &str, sha: &str) -> Result<()> {
use reqwest::blocking::get; use reqwest::blocking::get;
use std::fs::File; use std::fs::File;