diff --git a/virtweb_backend/Cargo.lock b/virtweb_backend/Cargo.lock index 2674e19..9d22341 100644 --- a/virtweb_backend/Cargo.lock +++ b/virtweb_backend/Cargo.lock @@ -1280,6 +1280,29 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" +[[package]] +name = "lazy-regex" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e723bd417b2df60a0f6a2b6825f297ea04b245d4ba52b5a22cb679bdf58b05fa" +dependencies = [ + "lazy-regex-proc_macros", + "once_cell", + "regex", +] + +[[package]] +name = "lazy-regex-proc_macros" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0a1d9139f0ee2e862e08a9c5d0ba0470f2aa21cd1e1aa1b1562f83116c725f" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.29", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1814,6 +1837,18 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-xml-rs" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782" +dependencies = [ + "log", + "serde", + "thiserror", + "xml-rs", +] + [[package]] name = "serde_derive" version = "1.0.188" @@ -1995,6 +2030,26 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "1.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + [[package]] name = "time" version = "0.3.28" @@ -2181,6 +2236,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "uuid" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +dependencies = [ + "getrandom", + "serde", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -2235,15 +2300,19 @@ dependencies = [ "clap", "env_logger", "futures-util", + "lazy-regex", "lazy_static", "light-openid", "log", "reqwest", "serde", + "serde-xml-rs", "serde_json", "sysinfo", "tempfile", + "thiserror", "url", + "uuid", "virt", ] @@ -2458,6 +2527,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "xml-rs" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab77e97b50aee93da431f2cee7cd0f43b4d1da3c408042f2d7d164187774f0a" + [[package]] name = "zstd" version = "0.12.4" diff --git a/virtweb_backend/Cargo.toml b/virtweb_backend/Cargo.toml index a9c01b8..e7c3aec 100644 --- a/virtweb_backend/Cargo.toml +++ b/virtweb_backend/Cargo.toml @@ -20,6 +20,7 @@ actix-cors = "0.6.4" actix-files = "0.6.2" serde = { version = "1.0.175", features = ["derive"] } serde_json = "1.0.105" +serde-xml-rs = "0.6.0" futures-util = "0.3.28" anyhow = "1.0.75" actix-multipart = "0.6.1" @@ -27,4 +28,7 @@ tempfile = "3.8.0" reqwest = { version = "0.11.18", features = ["stream"] } url = "2.4.0" virt = "0.3.0" -sysinfo = { version = "0.29.10", features = ["serde"] } \ No newline at end of file +sysinfo = { version = "0.29.10", features = ["serde"] } +uuid = { version = "1.4.1", features = ["v4", "serde"] } +lazy-regex = "3.0.2" +thiserror = "1.0.47" \ No newline at end of file diff --git a/virtweb_backend/src/actors/libvirt_actor.rs b/virtweb_backend/src/actors/libvirt_actor.rs index 5cf00ec..d2e2ab5 100644 --- a/virtweb_backend/src/actors/libvirt_actor.rs +++ b/virtweb_backend/src/actors/libvirt_actor.rs @@ -1,6 +1,9 @@ use crate::app_config::AppConfig; +use crate::libvirt_lib_structures::{DomainXML, DomainXMLUuid}; +use crate::libvirt_rest_structures::*; use actix::{Actor, Context, Handler, Message}; use virt::connect::Connect; +use virt::domain::Domain; pub struct LibVirtActor { m: Connect, @@ -28,30 +31,6 @@ impl Actor for LibVirtActor { #[rtype(result = "anyhow::Result")] pub struct GetHypervisorInfo; -#[derive(serde::Serialize)] -pub struct HypervisorInfo { - pub r#type: String, - pub hyp_version: u32, - pub lib_version: u32, - pub capabilities: String, - pub free_memory: u64, - pub hostname: String, - pub node: HypervisorNodeInfo, -} - -#[derive(serde::Serialize)] -pub struct HypervisorNodeInfo { - pub cpu_model: String, - /// Memory size in kilobytes - pub memory_size: u64, - pub number_of_active_cpus: u32, - pub cpu_frequency_mhz: u32, - pub number_of_numa_cell: u32, - pub number_of_cpu_socket_per_node: u32, - pub number_of_core_per_sockets: u32, - pub number_of_threads_per_core: u32, -} - impl Handler for LibVirtActor { type Result = anyhow::Result; @@ -77,3 +56,18 @@ impl Handler for LibVirtActor { }) } } + +#[derive(Message)] +#[rtype(result = "anyhow::Result")] +pub struct DefineDomainReq(pub DomainXML); + +impl Handler for LibVirtActor { + type Result = anyhow::Result; + + fn handle(&mut self, msg: DefineDomainReq, _ctx: &mut Self::Context) -> Self::Result { + let xml = serde_xml_rs::to_string(&msg.0)?; + log::debug!("Define domain:\n{}", xml); + let domain = Domain::define_xml(&self.m, &xml)?; + DomainXMLUuid::parse_from_str(&domain.get_uuid_string()?) + } +} diff --git a/virtweb_backend/src/constants.rs b/virtweb_backend/src/constants.rs index ec19c29..09e3aa3 100644 --- a/virtweb_backend/src/constants.rs +++ b/virtweb_backend/src/constants.rs @@ -25,3 +25,9 @@ pub const ALLOWED_ISO_MIME_TYPES: [&str; 3] = [ /// ISO max size pub const ISO_MAX_SIZE: usize = 10 * 1000 * 1000 * 1000; + +/// Min VM memory size (MB) +pub const MIN_VM_MEMORY: usize = 100; + +/// Max VM memory size (MB) +pub const MAX_VM_MEMORY: usize = 64000; diff --git a/virtweb_backend/src/controllers/mod.rs b/virtweb_backend/src/controllers/mod.rs index 5233a99..a3c4e25 100644 --- a/virtweb_backend/src/controllers/mod.rs +++ b/virtweb_backend/src/controllers/mod.rs @@ -8,6 +8,7 @@ use std::io::ErrorKind; pub mod auth_controller; pub mod iso_controller; pub mod server_controller; +pub mod vm_controller; /// Custom error to ease controller writing #[derive(Debug)] diff --git a/virtweb_backend/src/controllers/server_controller.rs b/virtweb_backend/src/controllers/server_controller.rs index 72d66df..ebced07 100644 --- a/virtweb_backend/src/controllers/server_controller.rs +++ b/virtweb_backend/src/controllers/server_controller.rs @@ -1,8 +1,8 @@ -use crate::actors::libvirt_actor::HypervisorInfo; use crate::app_config::AppConfig; use crate::constants; use crate::controllers::{HttpResult, LibVirtReq}; use crate::extractors::local_auth_extractor::LocalAuthEnabled; +use crate::libvirt_rest_structures::HypervisorInfo; use actix_web::{HttpResponse, Responder}; use sysinfo::{System, SystemExt}; diff --git a/virtweb_backend/src/controllers/vm_controller.rs b/virtweb_backend/src/controllers/vm_controller.rs new file mode 100644 index 0000000..6476128 --- /dev/null +++ b/virtweb_backend/src/controllers/vm_controller.rs @@ -0,0 +1,17 @@ +use crate::controllers::{HttpResult, LibVirtReq}; +use crate::libvirt_rest_structures::VMInfo; +use actix_web::{web, HttpResponse}; + +/// Create a new VM +pub async fn create(client: LibVirtReq, req: web::Json) -> HttpResult { + let domain = match req.0.to_domain() { + Ok(d) => d, + Err(e) => { + log::error!("Failed to extract domain info! {e}"); + return Ok(HttpResponse::BadRequest().body(e.to_string())); + } + }; + let id = client.update_domain(domain).await?; + + Ok(HttpResponse::Ok().json(id)) +} diff --git a/virtweb_backend/src/lib.rs b/virtweb_backend/src/lib.rs index 51ef95d..3d733aa 100644 --- a/virtweb_backend/src/lib.rs +++ b/virtweb_backend/src/lib.rs @@ -4,5 +4,7 @@ pub mod constants; pub mod controllers; pub mod extractors; pub mod libvirt_client; +pub mod libvirt_lib_structures; +pub mod libvirt_rest_structures; pub mod middlewares; pub mod utils; diff --git a/virtweb_backend/src/libvirt_client.rs b/virtweb_backend/src/libvirt_client.rs index ededcdc..69525ef 100644 --- a/virtweb_backend/src/libvirt_client.rs +++ b/virtweb_backend/src/libvirt_client.rs @@ -1,5 +1,7 @@ use crate::actors::libvirt_actor; -use crate::actors::libvirt_actor::{HypervisorInfo, LibVirtActor}; +use crate::actors::libvirt_actor::LibVirtActor; +use crate::libvirt_lib_structures::{DomainXML, DomainXMLUuid}; +use crate::libvirt_rest_structures::HypervisorInfo; use actix::Addr; #[derive(Clone)] @@ -10,4 +12,9 @@ impl LibVirtClient { pub async fn get_info(&self) -> anyhow::Result { self.0.send(libvirt_actor::GetHypervisorInfo).await? } + + /// Update a domain + pub async fn update_domain(&self, xml: DomainXML) -> anyhow::Result { + self.0.send(libvirt_actor::DefineDomainReq(xml)).await? + } } diff --git a/virtweb_backend/src/libvirt_lib_structures.rs b/virtweb_backend/src/libvirt_lib_structures.rs new file mode 100644 index 0000000..e9e75c1 --- /dev/null +++ b/virtweb_backend/src/libvirt_lib_structures.rs @@ -0,0 +1,58 @@ +#[derive(serde::Serialize, serde::Deserialize)] +pub struct DomainXMLUuid(pub uuid::Uuid); + +impl DomainXMLUuid { + pub fn parse_from_str(s: &str) -> anyhow::Result { + Ok(Self(uuid::Uuid::parse_str(s)?)) + } + + pub fn is_valid(&self) -> bool { + self.0.get_version_num() == 4 + } +} + +/// OS information +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "os")] +pub struct OSXML { + pub r#type: OSTypeXML, +} + +/// OS Type information +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "os")] +pub struct OSTypeXML { + #[serde(rename(serialize = "@arch"))] + pub arch: String, + #[serde(rename = "$value")] + pub body: String, +} + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "memory")] +pub struct DomainMemoryXML { + #[serde(rename(serialize = "@unit"))] + pub unit: String, + + #[serde(rename = "$value")] + pub memory: String, +} + +/// Domain information, see https://libvirt.org/formatdomain.html +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "domain")] +pub struct DomainXML { + /// Domain type (kvm) + #[serde(rename(serialize = "@type"))] + pub r#type: String, + + pub name: String, + pub uuid: Option, + pub genid: Option, + pub title: Option, + pub description: Option, + pub os: OSXML, + + /// The maximum allocation of memory for the guest at boot time + pub memory: DomainMemoryXML, +} diff --git a/virtweb_backend/src/libvirt_rest_structures.rs b/virtweb_backend/src/libvirt_rest_structures.rs new file mode 100644 index 0000000..0109177 --- /dev/null +++ b/virtweb_backend/src/libvirt_rest_structures.rs @@ -0,0 +1,118 @@ +use crate::constants; +use crate::libvirt_lib_structures::{DomainMemoryXML, DomainXML, DomainXMLUuid, OSTypeXML, OSXML}; +use crate::libvirt_rest_structures::LibVirtStructError::StructureExtractionError; +use lazy_regex::regex; + +#[derive(thiserror::Error, Debug)] +enum LibVirtStructError { + #[error("StructureExtractionError: {0}")] + StructureExtractionError(&'static str), +} + +#[derive(serde::Serialize)] +pub struct HypervisorInfo { + pub r#type: String, + pub hyp_version: u32, + pub lib_version: u32, + pub capabilities: String, + pub free_memory: u64, + pub hostname: String, + pub node: HypervisorNodeInfo, +} + +#[derive(serde::Serialize)] +pub struct HypervisorNodeInfo { + pub cpu_model: String, + /// Memory size in kilobytes + pub memory_size: u64, + pub number_of_active_cpus: u32, + pub cpu_frequency_mhz: u32, + pub number_of_numa_cell: u32, + pub number_of_cpu_socket_per_node: u32, + pub number_of_core_per_sockets: u32, + pub number_of_threads_per_core: u32, +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub enum BootType { + Legacy, + UEFI, + UEFISecureBoot, +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub enum VMArchitecture { + #[serde(rename = "i686")] + I686, + #[serde(rename = "x86_64")] + X86_64, +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct VMInfo { + /// VM name (alphanumeric characters only) + pub name: String, + pub uuid: Option, + pub genid: Option, + pub title: Option, + pub description: Option, + pub boot_type: Option, + pub architecture: VMArchitecture, + /// VM allocated memory, in megabytes + pub memory: usize, +} + +impl VMInfo { + /// Turn this VM into a domain + pub fn to_domain(self) -> anyhow::Result { + if !regex!("^[a-zA-Z0-9]+$").is_match(&self.name) { + return Err(StructureExtractionError("VM name is invalid!").into()); + } + + if let Some(n) = &self.uuid { + if n.is_valid() { + return Err(StructureExtractionError("VM UUID is invalid!").into()); + } + } + + if let Some(n) = &self.genid { + if n.is_valid() { + return Err(StructureExtractionError("VM genid is invalid!").into()); + } + } + + if let Some(n) = &self.title { + if n.contains('\n') { + return Err(StructureExtractionError("VM title contain newline char!").into()); + } + } + + if self.memory < constants::MIN_VM_MEMORY || self.memory > constants::MAX_VM_MEMORY { + return Err(StructureExtractionError("VM memory is invalid!").into()); + } + + Ok(DomainXML { + r#type: "kvm".to_string(), + name: self.name, + uuid: self.uuid, + genid: self.genid.map(|i| i.0), + title: self.title, + description: self.description, + + os: OSXML { + r#type: OSTypeXML { + arch: match self.architecture { + VMArchitecture::I686 => "i686", + VMArchitecture::X86_64 => "x86_64", + } + .to_string(), + body: "hvm".to_string(), + }, + }, + memory: DomainMemoryXML { + unit: "MB".to_string(), + memory: self.memory.to_string(), + }, + }) + } +} diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index 00f8f46..5323688 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -20,7 +20,9 @@ use virtweb_backend::constants; use virtweb_backend::constants::{ MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME, }; -use virtweb_backend::controllers::{auth_controller, iso_controller, server_controller}; +use virtweb_backend::controllers::{ + auth_controller, iso_controller, server_controller, vm_controller, +}; use virtweb_backend::libvirt_client::LibVirtClient; use virtweb_backend::middlewares::auth_middleware::AuthChecker; use virtweb_backend::utils::files_utils; @@ -132,6 +134,8 @@ async fn main() -> std::io::Result<()> { "/api/iso/{filename}", web::delete().to(iso_controller::delete_file), ) + // Virtual machines controller + .route("/api/vm/create", web::post().to(vm_controller::create)) }) .bind(&AppConfig::get().listen_address)? .run()