From b85feef0ab662e0c435d754eb70b7882d359a068 Mon Sep 17 00:00:00 2001 From: ghe0 Date: Sat, 14 Dec 2024 01:10:30 +0200 Subject: [PATCH] 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 --- prod_setting/8443_and_ipv6.yaml | 13 ++ prod_setting/ipv4_and_ipv6.yaml | 8 +- prod_setting/minimal_nat.yaml | 13 ++ prod_setting/only_ipv4.yaml | 13 ++ scripts/start_qemu_vm.sh | 24 +-- src/config.rs | 4 +- src/constants.rs | 1 + src/main.rs | 56 ++++- src/state.rs | 354 +++++++++++++++++++++++++++----- 9 files changed, 404 insertions(+), 82 deletions(-) create mode 100644 prod_setting/8443_and_ipv6.yaml create mode 100644 prod_setting/minimal_nat.yaml create mode 100644 prod_setting/only_ipv4.yaml diff --git a/prod_setting/8443_and_ipv6.yaml b/prod_setting/8443_and_ipv6.yaml new file mode 100644 index 0000000..de8f148 --- /dev/null +++ b/prod_setting/8443_and_ipv6.yaml @@ -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" diff --git a/prod_setting/ipv4_and_ipv6.yaml b/prod_setting/ipv4_and_ipv6.yaml index b6b7e13..2bc4744 100644 --- a/prod_setting/ipv4_and_ipv6.yaml +++ b/prod_setting/ipv4_and_ipv6.yaml @@ -1,5 +1,5 @@ -uuid: "0000000000000000000-0001" -hostname: "ghe-vm-1" +uuid: "uuid-0002" +hostname: "ghe-vm-2" admin_key: "MCowBQYDK2VwAyEAEoJ50VwJc7noWxylhioU2kk35MkO5as4U92UbP2A7xk=" extra_ports: [] public_ipv4: true @@ -9,5 +9,5 @@ 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=1r44_g-mi1PlFu5lmhN1kbuZdJgATINsg" -dtrfs_sha: "f347574bb637c306aaf0f8d0650efc90b95cdc61ea6c1c0f5b87b7d162aae4f3" +dtrfs_url: "https://drive.google.com/uc?export=download&id=1WoAVb9VS0rlzmuEIwwzZMx7N7SsUQf6q" +dtrfs_sha: "9fe9ae795a239a426a290f21dfa857182e3fe2fa556f8e8997ab35dfeb8fca24" diff --git a/prod_setting/minimal_nat.yaml b/prod_setting/minimal_nat.yaml new file mode 100644 index 0000000..ad536af --- /dev/null +++ b/prod_setting/minimal_nat.yaml @@ -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" diff --git a/prod_setting/only_ipv4.yaml b/prod_setting/only_ipv4.yaml new file mode 100644 index 0000000..a7b5987 --- /dev/null +++ b/prod_setting/only_ipv4.yaml @@ -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" diff --git a/scripts/start_qemu_vm.sh b/scripts/start_qemu_vm.sh index 46c0d38..0244ed1 100755 --- a/scripts/start_qemu_vm.sh +++ b/scripts/start_qemu_vm.sh @@ -4,7 +4,7 @@ echo "Environment variable VM_UUID is not set." exit 1 } -source "/etc/detee/daemon/vms/$VM_UUID" +source "/etc/detee/daemon/vms/${VM_UUID}.sh" mandatory_vars=("KERNEL" "INITRD" "PARAMS" "CPU_TYPE" \ "VCPUS" "MEMORY" "MAX_MEMORY" "DISK") @@ -15,9 +15,10 @@ for var in "${mandatory_vars[@]}"; do fi done -interfaces=$(env | grep -oE '^NETWORK_INTERFACE_[0-9]*') +interfaces=$(env | sort | grep -oE '^NETWORK_INTERFACE_[0-9]*') nat_configured="false" -vtap_fd_counter=3 +vtap_nic_count=1 +qemu_device_params="" while read -r interface; do interface_type="$( echo ${!interface} | cut -d '_' -f1 )" @@ -28,38 +29,37 @@ while read -r interface; do if [[ "$interface_type" == "macvtap" ]]; then ip link add link $interface_device name $interface_name type $interface_type mode bridge 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 + sysctl -w net.ipv6.conf.$interface_name.accept_ra=0 ip link set $interface_name up ip link set $interface_name promisc on vtap_index="$(cat /sys/class/net/${interface_name}/ifindex)" vtap_addr="$(cat /sys/class/net/${interface_name}/address)" - fd_number=$vtap_fd_counter 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=" - ((vtap_fd_counter++)) + qemu_device_params+=" -netdev tap,id=hostnet1,fd=${fd_number}" + qemu_device_params+=" -device virtio-net-pci,netdev=hostnet${vtap_nic_count},mac=${vtap_addr},romfile=" + ((vtap_nic_count++)) fi if [[ "$interface_type" == "NAT" && "$nat_configured" == "false" ]]; then ports="" 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 )" guest_port="$( echo $port_pair | cut -d ':' -f2 )" ports+=",hostfwd=tcp::${host_port}-:${guest_port}" done - qemu_device_params="-netdev user,id=vmnic${ports}" - qemu_device_params+=" -device virtio-net-pci,netdev=vmnic,romfile=" + qemu_device_params+=" -netdev user,id=natnic${ports}" + qemu_device_params+=" -device virtio-net-pci,netdev=natnic,romfile=" fi # TODO: also handle bridge device (when IPs are public, but the host is the gateway) done <<< "$( echo "$interfaces" )" -vm_disk="/root/dtrfs/arch-1-ghe0.qcow2" - qemu-system-x86_64 $qemu_device_params \ -enable-kvm -cpu $CPU_TYPE -vga none \ -machine q35,confidential-guest-support=sev0,memory-backend=ram1 \ diff --git a/src/config.rs b/src/config.rs index d0feab5..3d6335d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,7 +5,7 @@ use std::collections::HashSet; use std::net::{Ipv4Addr, Ipv6Addr}; use std::ops::Range; -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] pub struct Volume { pub path: String, pub max_reservation_gb: usize, @@ -83,7 +83,7 @@ mod range_format { } impl Config { - pub fn from_file(path: &str) -> Result> { + pub fn load_from_disk(path: &str) -> Result> { let content = std::fs::read_to_string(path)?; let config: Config = serde_yaml::from_str(&content)?; Ok(config) diff --git a/src/constants.rs b/src/constants.rs index b73eca0..c54fbb4 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -2,6 +2,7 @@ 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 USED_RESOURCES: &str = "/etc/detee/daemon/used_resources.yaml"; 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 START_VM_SCRIPT: &str = "/usr/local/bin/detee/start_qemu_vm.sh"; diff --git a/src/main.rs b/src/main.rs index fb99b10..c1b9226 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,60 @@ mod config; -mod state; mod constants; +mod state; mod tcontract; use crate::config::Config; use crate::state::NewVMRequest; +use crate::state::UpdateVMReq; +use std::fs::read_dir; fn main() -> Result<(), Box> { let args: Vec = std::env::args().collect(); - let config = Config::from_file(crate::constants::DAEMON_CONFIG_PATH)?; - let mut res = state::Resources::new(&config.volumes); - let new_vm_req = NewVMRequest::from_file(&args[1])?; - let vm = state::VM::new(new_vm_req, &config, &mut res); - println!("Got VM: {:#?}", vm); - println!("Starting VM... \n{:?}", vm.unwrap().start()); + let config = Config::load_from_disk(crate::constants::DAEMON_CONFIG_PATH)?; + let mut res = match state::Resources::load_from_disk() { + Ok(res) => res, + Err(e) => { + println!("Could not load resources from disk: {e:?}"); + 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(()) } diff --git a/src/state.rs b/src/state.rs index 7ac7ee4..c43c12a 100644 --- a/src/state.rs +++ b/src/state.rs @@ -4,7 +4,9 @@ use crate::constants::*; use anyhow::anyhow; use anyhow::Result; use serde::Deserialize; +use serde::Serialize; use sha2::{Digest, Sha256}; +use std::collections::HashMap; use std::collections::HashSet; use std::fs; use std::fs::remove_file; @@ -15,13 +17,14 @@ use std::net::{Ipv4Addr, Ipv6Addr}; use std::path::Path; use std::process::Command; -#[derive(Debug)] +#[derive(Serialize, Deserialize, Debug)] pub struct Resources { + existing_vms: HashSet, // QEMU does not support MHz limiation reserved_vcpus: usize, reserved_memory: usize, reserved_ports: HashSet, - storage_pools: Vec, + reserved_storage: HashMap, reserved_ips: HashSet, reserved_if_names: HashSet, // sha256sum -> absolute path @@ -29,6 +32,19 @@ pub struct 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 { + 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) -> Self { let mut storage_pools = Vec::new(); for config_vol in config_volumes.iter() { @@ -39,23 +55,32 @@ impl Resources { }); } let mut res = Resources { + existing_vms: HashSet::new(), reserved_vcpus: 0, reserved_memory: 0, reserved_ports: HashSet::new(), - storage_pools, + reserved_storage: HashMap::new(), reserved_ips: HashSet::new(), reserved_if_names: HashSet::new(), boot_files: HashSet::new(), }; res.scan_boot_files().unwrap(); + let _ = res.save_to_disk(); res } - fn available_storage_pool(&mut self, required_gb: usize) -> Option { - self.storage_pools.sort_by_key(|p| p.available_gb); - let pool = self.storage_pools.last()?; - if pool.available_gb > required_gb { - return Some(pool.path.clone()); + fn available_storage_pool(&mut self, required_gb: usize, config: &Config) -> Option { + let mut volumes = config.volumes.clone(); + for volume in volumes.iter_mut() { + if let Some(reservation) = self.reserved_storage.get(&volume.path) { + 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 } @@ -133,7 +158,6 @@ impl Resources { None } - // TODO: refactor this garbage cause it's only one char different from the previous one fn available_ipv6(&mut self, config: &Config) -> Option { for nic in config.network_interfaces.iter() { for range in nic.ipv6.iter() { @@ -198,6 +222,7 @@ impl Resources { } fn reserve_vm_resources(&mut self, vm: &VM) { + self.existing_vms.insert(vm.uuid.clone()); self.reserved_vcpus += vm.vcpus; self.reserved_memory += vm.memory_mb; for nic in vm.nics.iter() { @@ -212,15 +237,15 @@ impl Resources { self.reserved_ports.insert(*host_port); } - for storage_pool in self.storage_pools.iter_mut() { - if storage_pool.path == vm.storage_pool_path { - storage_pool.available_gb -= vm.disk_size_gb; - break; - } - } + self.reserved_storage + .entry(vm.storage_dir.clone()) + .and_modify(|gb| *gb += vm.disk_size_gb) + .or_insert(vm.disk_size_gb); + let _ = self.save_to_disk(); } fn free_vm_resources(&mut self, vm: &VM) { + self.existing_vms.remove(&vm.uuid); self.reserved_vcpus -= vm.vcpus; self.reserved_memory -= vm.memory_mb; for nic in vm.nics.iter() { @@ -234,10 +259,14 @@ impl Resources { for (host_port, _) in vm.fw_ports.iter() { 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 { path: String, available_gb: usize, @@ -245,12 +274,11 @@ pub struct StoragePool { // tier: StorageTier, } -#[derive(Debug)] +#[derive(Serialize, Deserialize, Debug)] 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 }, @@ -291,7 +319,7 @@ impl InterfaceConfig { } } -#[derive(Debug)] +#[derive(Serialize, Deserialize, Debug)] struct IPConfig { address: String, // requires short format (example: 24) @@ -299,7 +327,7 @@ struct IPConfig { gateway: String, } -#[derive(Debug)] +#[derive(Serialize, Deserialize, Debug)] pub struct VMNIC { if_config: InterfaceConfig, ips: Vec, @@ -311,9 +339,9 @@ impl VMNIC { } } -#[derive(Debug)] +#[derive(Serialize, Deserialize, Debug)] pub struct VM { - uuid: String, + pub uuid: String, hostname: String, admin_key: String, fw_ports: Vec<(u16, u16)>, @@ -325,7 +353,7 @@ pub struct VM { disk_size_gb: usize, kernel_sha: String, dtrfs_sha: String, - storage_pool_path: String, + storage_dir: String, } #[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> { + let content = std::fs::read_to_string(path)?; + let request: UpdateVMReq = serde_yaml::from_str(&content)?; + Ok(request) + } +} + #[derive(Debug)] pub enum VMCreationErrors { + VMAlreadyExists, NATandIPv4Conflict, TooManyCores, NotEnoughPorts, @@ -363,7 +408,10 @@ pub enum VMCreationErrors { NotEnoughStorage, IPv4NotAvailable, IPv6NotAvailable, + DiskTooSmall, + ServerDiskError(String), BootFileError(String), + HypervizorError(String), } impl VM { @@ -372,6 +420,9 @@ impl VM { config: &Config, res: &mut Resources, ) -> Result { + if res.existing_vms.contains(&req.uuid) { + return Err(VMCreationErrors::VMAlreadyExists); + } if req.extra_ports.len() > 0 && req.public_ipv4 { return Err(VMCreationErrors::NATandIPv4Conflict); } @@ -384,6 +435,9 @@ impl VM { if config.max_mem_reservation_mb < res.reserved_memory.saturating_add(req.memory_mb) { return Err(VMCreationErrors::NotEnoughMemory); } + if req.disk_size_gb < 4 { + return Err(VMCreationErrors::DiskTooSmall); + } if let Err(kernel_file_error) = 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, None => return Err(VMCreationErrors::NotEnoughStorage), }; @@ -456,40 +510,103 @@ impl VM { kernel_sha: req.kernel_sha, dtrfs_sha: req.dtrfs_sha, 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); 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<()> { self.create_disk()?; - self.write_vm_config()?; + self.write_sh_exports()?; self.write_systemd_unit_file()?; - // TODO: systemctl daemon-reload - // TODO: systemctl start - // TODO: systemctl enable + systemctl_reload()?; + systemctl_start_and_enable(&self.uuid)?; Ok(()) } pub fn delete(&self, res: &mut Resources) -> Result<()> { - // TODO: systemctl stop - // TODO: systemctl disable + systemctl_stop_and_disable(&self.uuid)?; res.free_vm_resources(&self); self.delete_systemd_unit_file()?; - // TODO: systemctl daemon-reload + systemctl_reload()?; self.delete_disk()?; - self.delete_vm_config()?; + self.delete_sh_exports()?; self.delete_vtap_interfaces()?; - Ok(()) - } - - pub fn deploy(&self) -> Result<()> { - self.create_disk()?; - self.write_vm_config()?; - self.write_systemd_unit_file()?; - // TODO: daemon-reload, systemctl enable, systemctl start + self.delete_config()?; Ok(()) } @@ -497,9 +614,9 @@ impl VM { // 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 { - let dir = match self.storage_pool_path.ends_with("/") { - true => self.storage_pool_path.clone(), - false => self.storage_pool_path.clone() + "/", + let dir = match self.storage_dir.ends_with("/") { + true => self.storage_dir.clone(), + false => self.storage_dir.clone() + "/", }; dir + &self.uuid + ".qcow2" } @@ -507,6 +624,13 @@ impl VM { pub fn kernel_params(&self) -> String { let mut ip_string = String::new(); 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 ip in nic.ips.iter() { ip_string += &format!( @@ -521,13 +645,28 @@ impl VM { 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 i = 0; for nic in self.nics.iter() { 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 if let Some(vtap_name) = nic.if_config.vtap_name() { interface += &format!("_{}_{}", nic.if_config.device_name(), vtap_name); @@ -543,8 +682,9 @@ impl VM { ports += &format!("{}:{} ", port.0, port.1); } if ports != "" { - vars += &format!(r#"NAT_PORT_FW="{}""#, ports.trim_end()); + vars += &format!(r#"export NAT_PORT_FW="{}""#, ports.trim_end()); vars += "\n"; + vars += "export NETWORK_INTERFACE_0000=NAT\n"; } vars += &format!( @@ -570,17 +710,17 @@ impl VM { vars += &format!(r#"export DISK="{}""#, self.disk_path()); 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())?; Ok(()) } - pub fn delete_vm_config(&self) -> Result<()> { - remove_file(VM_CONFIG_DIR.to_string() + &self.uuid)?; + fn delete_sh_exports(&self) -> Result<()> { + remove_file(VM_CONFIG_DIR.to_string() + &self.uuid + ".sh")?; Ok(()) } - pub fn delete_vtap_interfaces(&self) -> Result<()> { + 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") @@ -601,7 +741,7 @@ impl VM { Ok(()) } - pub fn write_systemd_unit_file(&self) -> Result<()> { + fn write_systemd_unit_file(&self) -> Result<()> { let mut contents = String::new(); contents += &format!("[Unit]\n"); contents += &format!("Description=DeTEE {}\n", self.uuid); @@ -617,17 +757,25 @@ impl VM { contents += &format!("[Install]\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())?; Ok(()) } - pub fn delete_systemd_unit_file(&self) -> Result<()> { - remove_file("/tmp/etc/systemd/system/".to_string() + &self.uuid + ".service")?; + fn delete_systemd_unit_file(&self) -> Result<()> { + remove_file("/etc/systemd/system/".to_string() + &self.uuid + ".service")?; 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") .arg("create") .arg("-f") @@ -647,12 +795,104 @@ impl VM { 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())?; 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<()> { use reqwest::blocking::get; use std::fs::File;