From bcf6e8a33b24db34f3fb980b11fbb1c86e7b1e8c Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 25 Nov 2024 19:19:33 +0100 Subject: [PATCH 1/7] Add routes to implement --- virtweb_backend/src/main.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index 5797989..58cc251 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -212,6 +212,14 @@ async fn main() -> std::io::Result<()> { .route("/api/vnc", web::get().to(vm_controller::vnc)) // Groups controller .route("/api/group/list", web::get().to(groups_controller::list)) + // TODO list VM + // TODO start VM + // TODO stop VM + // TODO suspend VM + // TODO resume VM + // TODO kill VM + // TODO reset VM + // TODO screenshot VM // Network controller .route( "/api/network/create", -- 2.45.2 From 9f8e2e9dc24dc616ec23d70dd35af314dbac2f0b Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 25 Nov 2024 20:51:01 +0100 Subject: [PATCH 2/7] Can get the list of VM of a group --- .../src/controllers/groups_controller.rs | 11 ++++ .../src/extractors/group_vm_id_extractor.rs | 66 +++++++++++++++++++ virtweb_backend/src/extractors/mod.rs | 1 + virtweb_backend/src/libvirt_client.rs | 15 +++++ .../src/libvirt_lib_structures/domain.rs | 60 ++++++++--------- .../src/libvirt_lib_structures/mod.rs | 2 +- virtweb_backend/src/main.rs | 5 +- 7 files changed, 128 insertions(+), 32 deletions(-) create mode 100644 virtweb_backend/src/extractors/group_vm_id_extractor.rs diff --git a/virtweb_backend/src/controllers/groups_controller.rs b/virtweb_backend/src/controllers/groups_controller.rs index b9b6c12..b61cc25 100644 --- a/virtweb_backend/src/controllers/groups_controller.rs +++ b/virtweb_backend/src/controllers/groups_controller.rs @@ -1,4 +1,6 @@ use crate::controllers::{HttpResult, LibVirtReq}; +use crate::extractors::group_vm_id_extractor::GroupVmIdExtractor; +use crate::libvirt_rest_structures::vm::VMInfo; use actix_web::HttpResponse; /// Get the list of groups @@ -14,3 +16,12 @@ pub async fn list(client: LibVirtReq) -> HttpResult { Ok(HttpResponse::Ok().json(groups)) } + +/// Get information about a VM +pub async fn vm_info(vms_ids: GroupVmIdExtractor) -> HttpResult { + let mut vms = Vec::new(); + for vm in vms_ids.0 { + vms.push(VMInfo::from_domain(vm)?) + } + Ok(HttpResponse::Ok().json(vms)) +} diff --git a/virtweb_backend/src/extractors/group_vm_id_extractor.rs b/virtweb_backend/src/extractors/group_vm_id_extractor.rs new file mode 100644 index 0000000..ac85cf0 --- /dev/null +++ b/virtweb_backend/src/extractors/group_vm_id_extractor.rs @@ -0,0 +1,66 @@ +use crate::controllers::LibVirtReq; +use crate::libvirt_lib_structures::domain::DomainXML; +use crate::libvirt_lib_structures::XMLUuid; +use crate::libvirt_rest_structures::vm::VMGroupId; +use actix_http::Payload; +use actix_web::error::ErrorBadRequest; +use actix_web::web::Query; +use actix_web::{web, Error, FromRequest, HttpRequest}; +use std::future::Future; +use std::pin::Pin; + +pub struct GroupVmIdExtractor(pub Vec); + +#[derive(serde::Deserialize)] +struct GroupIDInPath { + gid: VMGroupId, +} + +#[derive(serde::Deserialize)] +struct FilterVM { + vm_id: Option, +} + +impl FromRequest for GroupVmIdExtractor { + type Error = Error; + type Future = Pin>>>; + + fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { + let req = req.clone(); + + Box::pin(async move { + let Ok(group_id) = + web::Path::::from_request(&req, &mut Payload::None).await + else { + return Err(ErrorBadRequest("Group ID not specified in path!")); + }; + let group_id = group_id.into_inner().gid; + + let filter_vm = match Query::::from_request(&req, &mut Payload::None).await { + Ok(v) => v, + Err(e) => { + log::error!("Failed to extract VM id from request! {e}"); + return Err(ErrorBadRequest("Failed to extract VM id from request!")); + } + }; + + let Ok(client) = LibVirtReq::from_request(&req, &mut Payload::None).await else { + return Err(ErrorBadRequest("Failed to extract client handle!")); + }; + + let vms = match client.get_full_group_vm_list(&group_id).await { + Ok(vms) => vms, + Err(e) => { + log::error!("Failed to get the VM of the group {group_id:?}: {e}"); + return Err(ErrorBadRequest("Failed to get the VM of the group!")); + } + }; + + // Filter (if requested by the user) + Ok(GroupVmIdExtractor(match filter_vm.vm_id { + None => vms, + Some(id) => vms.into_iter().filter(|vms| vms.uuid == Some(id)).collect(), + })) + }) + } +} diff --git a/virtweb_backend/src/extractors/mod.rs b/virtweb_backend/src/extractors/mod.rs index d284d19..491626d 100644 --- a/virtweb_backend/src/extractors/mod.rs +++ b/virtweb_backend/src/extractors/mod.rs @@ -1,3 +1,4 @@ pub mod api_auth_extractor; pub mod auth_extractor; +pub mod group_vm_id_extractor; pub mod local_auth_extractor; diff --git a/virtweb_backend/src/libvirt_client.rs b/virtweb_backend/src/libvirt_client.rs index 9cbe77c..2f9290e 100644 --- a/virtweb_backend/src/libvirt_client.rs +++ b/virtweb_backend/src/libvirt_client.rs @@ -122,6 +122,21 @@ impl LibVirtClient { Ok(out) } + /// Get the full list of VMs of a given group + pub async fn get_full_group_vm_list( + &self, + group: &VMGroupId, + ) -> anyhow::Result> { + let vms = self.get_full_domains_list().await?; + let mut out = Vec::new(); + for vm in vms { + if VMInfo::from_domain(vm.clone())?.group == Some(group.clone()) { + out.push(vm); + } + } + Ok(out) + } + /// Update a network configuration pub async fn update_network( &self, diff --git a/virtweb_backend/src/libvirt_lib_structures/domain.rs b/virtweb_backend/src/libvirt_lib_structures/domain.rs index f8ec61e..7aab273 100644 --- a/virtweb_backend/src/libvirt_lib_structures/domain.rs +++ b/virtweb_backend/src/libvirt_lib_structures/domain.rs @@ -19,7 +19,7 @@ pub struct DomainMetadataXML { } /// OS information -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "os")] pub struct OSXML { #[serde(rename = "@firmware", default)] @@ -29,7 +29,7 @@ pub struct OSXML { } /// OS Type information -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "os")] pub struct OSTypeXML { #[serde(rename = "@arch")] @@ -41,7 +41,7 @@ pub struct OSTypeXML { } /// OS Loader information -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "loader")] pub struct OSLoaderXML { #[serde(rename = "@secure")] @@ -49,39 +49,39 @@ pub struct OSLoaderXML { } /// Hypervisor features -#[derive(serde::Serialize, serde::Deserialize, Default, Debug)] +#[derive(serde::Serialize, serde::Deserialize, Clone, Default, Debug)] #[serde(rename = "features")] pub struct FeaturesXML { pub acpi: ACPIXML, } /// ACPI feature -#[derive(serde::Serialize, serde::Deserialize, Default, Debug)] +#[derive(serde::Serialize, serde::Deserialize, Clone, Default, Debug)] #[serde(rename = "acpi")] pub struct ACPIXML {} -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "mac")] pub struct NetMacAddress { #[serde(rename = "@address")] pub address: String, } -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "source")] pub struct NetIntSourceXML { #[serde(rename = "@network")] pub network: String, } -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "model")] pub struct NetIntModelXML { #[serde(rename = "@type")] pub r#type: String, } -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "filterref")] pub struct NetIntFilterParameterXML { #[serde(rename = "@name")] @@ -90,7 +90,7 @@ pub struct NetIntFilterParameterXML { pub value: String, } -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "filterref")] pub struct NetIntfilterRefXML { #[serde(rename = "@filter")] @@ -99,7 +99,7 @@ pub struct NetIntfilterRefXML { pub parameters: Vec, } -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "interface")] pub struct DomainNetInterfaceXML { #[serde(rename = "@type")] @@ -113,14 +113,14 @@ pub struct DomainNetInterfaceXML { pub filterref: Option, } -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "input")] pub struct DomainInputXML { #[serde(rename = "@type")] pub r#type: String, } -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "backend")] pub struct TPMBackendXML { #[serde(rename = "@type")] @@ -130,7 +130,7 @@ pub struct TPMBackendXML { pub r#version: String, } -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "tpm")] pub struct TPMDeviceXML { #[serde(rename = "@model")] @@ -139,7 +139,7 @@ pub struct TPMDeviceXML { } /// Devices information -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "devices")] pub struct DevicesXML { /// Graphics (used for VNC) @@ -168,7 +168,7 @@ pub struct DevicesXML { } /// Graphics information -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "graphics")] pub struct GraphicsXML { #[serde(rename = "@type")] @@ -178,14 +178,14 @@ pub struct GraphicsXML { } /// Video device information -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "video")] pub struct VideoXML { pub model: VideoModelXML, } /// Video model device information -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "model")] pub struct VideoModelXML { #[serde(rename = "@type")] @@ -193,7 +193,7 @@ pub struct VideoModelXML { } /// Disk information -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "disk")] pub struct DiskXML { #[serde(rename = "@type")] @@ -211,7 +211,7 @@ pub struct DiskXML { pub address: Option, } -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "driver")] pub struct DiskDriverXML { #[serde(rename = "@name")] @@ -222,14 +222,14 @@ pub struct DiskDriverXML { pub r#cache: String, } -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "source")] pub struct DiskSourceXML { #[serde(rename = "@file")] pub file: String, } -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "target")] pub struct DiskTargetXML { #[serde(rename = "@dev")] @@ -238,18 +238,18 @@ pub struct DiskTargetXML { pub bus: String, } -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "readonly")] pub struct DiskReadOnlyXML {} -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "boot")] pub struct DiskBootXML { #[serde(rename = "@order")] pub order: String, } -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "address")] pub struct DiskAddressXML { #[serde(rename = "@type")] @@ -269,7 +269,7 @@ pub struct DiskAddressXML { } /// Domain RAM information -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "memory")] pub struct DomainMemoryXML { #[serde(rename = "@unit")] @@ -279,7 +279,7 @@ pub struct DomainMemoryXML { pub memory: usize, } -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "topology")] pub struct DomainCPUTopology { #[serde(rename = "@sockets")] @@ -290,14 +290,14 @@ pub struct DomainCPUTopology { pub threads: usize, } -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "cpu")] pub struct DomainVCPUXML { #[serde(rename = "$value")] pub body: usize, } -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "cpu")] pub struct DomainCPUXML { #[serde(rename = "@mode")] @@ -306,7 +306,7 @@ pub struct DomainCPUXML { } /// Domain information, see https://libvirt.org/formatdomain.html -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename = "domain")] pub struct DomainXML { /// Domain type (kvm) diff --git a/virtweb_backend/src/libvirt_lib_structures/mod.rs b/virtweb_backend/src/libvirt_lib_structures/mod.rs index 59adfa6..65e7491 100644 --- a/virtweb_backend/src/libvirt_lib_structures/mod.rs +++ b/virtweb_backend/src/libvirt_lib_structures/mod.rs @@ -1,4 +1,4 @@ -#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)] +#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug, Eq, PartialEq)] pub struct XMLUuid(pub uuid::Uuid); impl XMLUuid { diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index 58cc251..0cf3a50 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -212,7 +212,10 @@ async fn main() -> std::io::Result<()> { .route("/api/vnc", web::get().to(vm_controller::vnc)) // Groups controller .route("/api/group/list", web::get().to(groups_controller::list)) - // TODO list VM + .route( + "/api/group/{gid}/vm/info", + web::get().to(groups_controller::vm_info), + ) // TODO start VM // TODO stop VM // TODO suspend VM -- 2.45.2 From 4b97fc498f0b662d2caa281eda39b5354f67f657 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 25 Nov 2024 20:59:49 +0100 Subject: [PATCH 3/7] Can start, shutdown, kill and reset VMs of groups --- .../src/controllers/groups_controller.rs | 42 ++++++++++++++++++- .../src/extractors/group_vm_id_extractor.rs | 4 +- virtweb_backend/src/main.rs | 20 +++++++-- 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/virtweb_backend/src/controllers/groups_controller.rs b/virtweb_backend/src/controllers/groups_controller.rs index b61cc25..b041454 100644 --- a/virtweb_backend/src/controllers/groups_controller.rs +++ b/virtweb_backend/src/controllers/groups_controller.rs @@ -17,7 +17,7 @@ pub async fn list(client: LibVirtReq) -> HttpResult { Ok(HttpResponse::Ok().json(groups)) } -/// Get information about a VM +/// Get information about the VMs of a group pub async fn vm_info(vms_ids: GroupVmIdExtractor) -> HttpResult { let mut vms = Vec::new(); for vm in vms_ids.0 { @@ -25,3 +25,43 @@ pub async fn vm_info(vms_ids: GroupVmIdExtractor) -> HttpResult { } Ok(HttpResponse::Ok().json(vms)) } + +/// Start the VMs of a group +pub async fn vm_start(client: LibVirtReq, vms_ids: GroupVmIdExtractor) -> HttpResult { + for vm in vms_ids.0 { + if let Some(uuid) = vm.uuid { + client.start_domain(uuid).await?; + } + } + Ok(HttpResponse::Ok().finish()) +} + +/// Shutdown the VMs of a group +pub async fn vm_shutdown(client: LibVirtReq, vms_ids: GroupVmIdExtractor) -> HttpResult { + for vm in vms_ids.0 { + if let Some(uuid) = vm.uuid { + client.shutdown_domain(uuid).await?; + } + } + Ok(HttpResponse::Ok().finish()) +} + +/// Kill the VMs of a group +pub async fn vm_kill(client: LibVirtReq, vms_ids: GroupVmIdExtractor) -> HttpResult { + for vm in vms_ids.0 { + if let Some(uuid) = vm.uuid { + client.kill_domain(uuid).await?; + } + } + Ok(HttpResponse::Ok().finish()) +} + +/// Reset the VMs of a group +pub async fn vm_reset(client: LibVirtReq, vms_ids: GroupVmIdExtractor) -> HttpResult { + for vm in vms_ids.0 { + if let Some(uuid) = vm.uuid { + client.reset_domain(uuid).await?; + } + } + Ok(HttpResponse::Ok().finish()) +} diff --git a/virtweb_backend/src/extractors/group_vm_id_extractor.rs b/virtweb_backend/src/extractors/group_vm_id_extractor.rs index ac85cf0..b5b823d 100644 --- a/virtweb_backend/src/extractors/group_vm_id_extractor.rs +++ b/virtweb_backend/src/extractors/group_vm_id_extractor.rs @@ -51,8 +51,8 @@ impl FromRequest for GroupVmIdExtractor { let vms = match client.get_full_group_vm_list(&group_id).await { Ok(vms) => vms, Err(e) => { - log::error!("Failed to get the VM of the group {group_id:?}: {e}"); - return Err(ErrorBadRequest("Failed to get the VM of the group!")); + log::error!("Failed to get the VMs of the group {group_id:?}: {e}"); + return Err(ErrorBadRequest("Failed to get the VMs of the group!")); } }; diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index 0cf3a50..9f9a76d 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -216,13 +216,27 @@ async fn main() -> std::io::Result<()> { "/api/group/{gid}/vm/info", web::get().to(groups_controller::vm_info), ) - // TODO start VM - // TODO stop VM + .route( + "/api/group/{gid}/vm/start", + web::get().to(groups_controller::vm_start), + ) + .route( + "/api/group/{gid}/vm/shutdown", + web::get().to(groups_controller::vm_shutdown), + ) // TODO suspend VM // TODO resume VM - // TODO kill VM + .route( + "/api/group/{gid}/vm/kill", + web::get().to(groups_controller::vm_kill), + ) + .route( + "/api/group/{gid}/vm/reset", + web::get().to(groups_controller::vm_reset), + ) // TODO reset VM // TODO screenshot VM + // TODO state of VM // Network controller .route( "/api/network/create", -- 2.45.2 From 200ccd764deb6e753a51ccdb22a060dcbf874d84 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 25 Nov 2024 21:11:33 +0100 Subject: [PATCH 4/7] Can suspend and resume VMs --- .../src/controllers/groups_controller.rs | 66 ++++++++++++++++--- virtweb_backend/src/main.rs | 11 +++- 2 files changed, 66 insertions(+), 11 deletions(-) diff --git a/virtweb_backend/src/controllers/groups_controller.rs b/virtweb_backend/src/controllers/groups_controller.rs index b041454..ae096f5 100644 --- a/virtweb_backend/src/controllers/groups_controller.rs +++ b/virtweb_backend/src/controllers/groups_controller.rs @@ -26,42 +26,92 @@ pub async fn vm_info(vms_ids: GroupVmIdExtractor) -> HttpResult { Ok(HttpResponse::Ok().json(vms)) } +#[derive(Default, serde::Serialize)] +pub struct TreatmentResult { + ok: usize, + failed: usize, +} + /// Start the VMs of a group pub async fn vm_start(client: LibVirtReq, vms_ids: GroupVmIdExtractor) -> HttpResult { + let mut res = TreatmentResult::default(); for vm in vms_ids.0 { if let Some(uuid) = vm.uuid { - client.start_domain(uuid).await?; + match client.start_domain(uuid).await { + Ok(_) => res.ok += 1, + Err(_) => res.failed += 1, + } } } - Ok(HttpResponse::Ok().finish()) + Ok(HttpResponse::Ok().json(res)) } /// Shutdown the VMs of a group pub async fn vm_shutdown(client: LibVirtReq, vms_ids: GroupVmIdExtractor) -> HttpResult { + let mut res = TreatmentResult::default(); for vm in vms_ids.0 { if let Some(uuid) = vm.uuid { - client.shutdown_domain(uuid).await?; + match client.shutdown_domain(uuid).await { + Ok(_) => res.ok += 1, + Err(_) => res.failed += 1, + } } } - Ok(HttpResponse::Ok().finish()) + Ok(HttpResponse::Ok().json(res)) +} + +/// Suspend the VMs of a group +pub async fn vm_suspend(client: LibVirtReq, vms_ids: GroupVmIdExtractor) -> HttpResult { + let mut res = TreatmentResult::default(); + for vm in vms_ids.0 { + if let Some(uuid) = vm.uuid { + match client.suspend_domain(uuid).await { + Ok(_) => res.ok += 1, + Err(_) => res.failed += 1, + } + } + } + Ok(HttpResponse::Ok().json(res)) +} + +/// Resume the VMs of a group +pub async fn vm_resume(client: LibVirtReq, vms_ids: GroupVmIdExtractor) -> HttpResult { + let mut res = TreatmentResult::default(); + for vm in vms_ids.0 { + if let Some(uuid) = vm.uuid { + match client.resume_domain(uuid).await { + Ok(_) => res.ok += 1, + Err(_) => res.failed += 1, + } + } + } + Ok(HttpResponse::Ok().json(res)) } /// Kill the VMs of a group pub async fn vm_kill(client: LibVirtReq, vms_ids: GroupVmIdExtractor) -> HttpResult { + let mut res = TreatmentResult::default(); for vm in vms_ids.0 { if let Some(uuid) = vm.uuid { - client.kill_domain(uuid).await?; + match client.kill_domain(uuid).await { + Ok(_) => res.ok += 1, + Err(_) => res.failed += 1, + } } } - Ok(HttpResponse::Ok().finish()) + Ok(HttpResponse::Ok().json(res)) } /// Reset the VMs of a group pub async fn vm_reset(client: LibVirtReq, vms_ids: GroupVmIdExtractor) -> HttpResult { + let mut res = TreatmentResult::default(); for vm in vms_ids.0 { if let Some(uuid) = vm.uuid { - client.reset_domain(uuid).await?; + match client.reset_domain(uuid).await { + Ok(_) => res.ok += 1, + Err(_) => res.failed += 1, + } } } - Ok(HttpResponse::Ok().finish()) + Ok(HttpResponse::Ok().json(res)) } diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index 9f9a76d..7a4c13d 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -224,8 +224,14 @@ async fn main() -> std::io::Result<()> { "/api/group/{gid}/vm/shutdown", web::get().to(groups_controller::vm_shutdown), ) - // TODO suspend VM - // TODO resume VM + .route( + "/api/group/{gid}/vm/suspend", + web::get().to(groups_controller::vm_suspend), + ) + .route( + "/api/group/{gid}/vm/resume", + web::get().to(groups_controller::vm_resume), + ) .route( "/api/group/{gid}/vm/kill", web::get().to(groups_controller::vm_kill), @@ -234,7 +240,6 @@ async fn main() -> std::io::Result<()> { "/api/group/{gid}/vm/reset", web::get().to(groups_controller::vm_reset), ) - // TODO reset VM // TODO screenshot VM // TODO state of VM // Network controller -- 2.45.2 From 7eced6b8b5dc52420c4701eac19f01b407bac43e Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 25 Nov 2024 21:26:28 +0100 Subject: [PATCH 5/7] Can get the state of VMs of a group --- .../src/controllers/groups_controller.rs | 42 ++++++++++++------- .../src/libvirt_lib_structures/mod.rs | 2 +- virtweb_backend/src/main.rs | 5 ++- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/virtweb_backend/src/controllers/groups_controller.rs b/virtweb_backend/src/controllers/groups_controller.rs index ae096f5..bab92cc 100644 --- a/virtweb_backend/src/controllers/groups_controller.rs +++ b/virtweb_backend/src/controllers/groups_controller.rs @@ -2,6 +2,7 @@ use crate::controllers::{HttpResult, LibVirtReq}; use crate::extractors::group_vm_id_extractor::GroupVmIdExtractor; use crate::libvirt_rest_structures::vm::VMInfo; use actix_web::HttpResponse; +use std::collections::HashMap; /// Get the list of groups pub async fn list(client: LibVirtReq) -> HttpResult { @@ -18,9 +19,9 @@ pub async fn list(client: LibVirtReq) -> HttpResult { } /// Get information about the VMs of a group -pub async fn vm_info(vms_ids: GroupVmIdExtractor) -> HttpResult { +pub async fn vm_info(vms_xml: GroupVmIdExtractor) -> HttpResult { let mut vms = Vec::new(); - for vm in vms_ids.0 { + for vm in vms_xml.0 { vms.push(VMInfo::from_domain(vm)?) } Ok(HttpResponse::Ok().json(vms)) @@ -33,9 +34,9 @@ pub struct TreatmentResult { } /// Start the VMs of a group -pub async fn vm_start(client: LibVirtReq, vms_ids: GroupVmIdExtractor) -> HttpResult { +pub async fn vm_start(client: LibVirtReq, vms: GroupVmIdExtractor) -> HttpResult { let mut res = TreatmentResult::default(); - for vm in vms_ids.0 { + for vm in vms.0 { if let Some(uuid) = vm.uuid { match client.start_domain(uuid).await { Ok(_) => res.ok += 1, @@ -47,9 +48,9 @@ pub async fn vm_start(client: LibVirtReq, vms_ids: GroupVmIdExtractor) -> HttpRe } /// Shutdown the VMs of a group -pub async fn vm_shutdown(client: LibVirtReq, vms_ids: GroupVmIdExtractor) -> HttpResult { +pub async fn vm_shutdown(client: LibVirtReq, vms: GroupVmIdExtractor) -> HttpResult { let mut res = TreatmentResult::default(); - for vm in vms_ids.0 { + for vm in vms.0 { if let Some(uuid) = vm.uuid { match client.shutdown_domain(uuid).await { Ok(_) => res.ok += 1, @@ -61,9 +62,9 @@ pub async fn vm_shutdown(client: LibVirtReq, vms_ids: GroupVmIdExtractor) -> Htt } /// Suspend the VMs of a group -pub async fn vm_suspend(client: LibVirtReq, vms_ids: GroupVmIdExtractor) -> HttpResult { +pub async fn vm_suspend(client: LibVirtReq, vms: GroupVmIdExtractor) -> HttpResult { let mut res = TreatmentResult::default(); - for vm in vms_ids.0 { + for vm in vms.0 { if let Some(uuid) = vm.uuid { match client.suspend_domain(uuid).await { Ok(_) => res.ok += 1, @@ -75,9 +76,9 @@ pub async fn vm_suspend(client: LibVirtReq, vms_ids: GroupVmIdExtractor) -> Http } /// Resume the VMs of a group -pub async fn vm_resume(client: LibVirtReq, vms_ids: GroupVmIdExtractor) -> HttpResult { +pub async fn vm_resume(client: LibVirtReq, vms: GroupVmIdExtractor) -> HttpResult { let mut res = TreatmentResult::default(); - for vm in vms_ids.0 { + for vm in vms.0 { if let Some(uuid) = vm.uuid { match client.resume_domain(uuid).await { Ok(_) => res.ok += 1, @@ -89,9 +90,9 @@ pub async fn vm_resume(client: LibVirtReq, vms_ids: GroupVmIdExtractor) -> HttpR } /// Kill the VMs of a group -pub async fn vm_kill(client: LibVirtReq, vms_ids: GroupVmIdExtractor) -> HttpResult { +pub async fn vm_kill(client: LibVirtReq, vms: GroupVmIdExtractor) -> HttpResult { let mut res = TreatmentResult::default(); - for vm in vms_ids.0 { + for vm in vms.0 { if let Some(uuid) = vm.uuid { match client.kill_domain(uuid).await { Ok(_) => res.ok += 1, @@ -103,9 +104,9 @@ pub async fn vm_kill(client: LibVirtReq, vms_ids: GroupVmIdExtractor) -> HttpRes } /// Reset the VMs of a group -pub async fn vm_reset(client: LibVirtReq, vms_ids: GroupVmIdExtractor) -> HttpResult { +pub async fn vm_reset(client: LibVirtReq, vms: GroupVmIdExtractor) -> HttpResult { let mut res = TreatmentResult::default(); - for vm in vms_ids.0 { + for vm in vms.0 { if let Some(uuid) = vm.uuid { match client.reset_domain(uuid).await { Ok(_) => res.ok += 1, @@ -115,3 +116,16 @@ pub async fn vm_reset(client: LibVirtReq, vms_ids: GroupVmIdExtractor) -> HttpRe } Ok(HttpResponse::Ok().json(res)) } + +/// Get the state of the VMs +pub async fn vm_state(client: LibVirtReq, vms: GroupVmIdExtractor) -> HttpResult { + let mut states = HashMap::new(); + + for vm in vms.0 { + if let Some(uuid) = vm.uuid { + states.insert(uuid.clone(), client.get_domain_state(uuid).await?); + } + } + + Ok(HttpResponse::Ok().json(states)) +} diff --git a/virtweb_backend/src/libvirt_lib_structures/mod.rs b/virtweb_backend/src/libvirt_lib_structures/mod.rs index 65e7491..8fbca27 100644 --- a/virtweb_backend/src/libvirt_lib_structures/mod.rs +++ b/virtweb_backend/src/libvirt_lib_structures/mod.rs @@ -1,4 +1,4 @@ -#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug, Eq, PartialEq)] +#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug, Eq, PartialEq, Hash)] pub struct XMLUuid(pub uuid::Uuid); impl XMLUuid { diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index 7a4c13d..a8e18f2 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -241,7 +241,10 @@ async fn main() -> std::io::Result<()> { web::get().to(groups_controller::vm_reset), ) // TODO screenshot VM - // TODO state of VM + .route( + "/api/group/{gid}/vm/state", + web::get().to(groups_controller::vm_state), + ) // Network controller .route( "/api/network/create", -- 2.45.2 From 1c6ca2d76a55ffdcca58921166f2c9cf67497534 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 25 Nov 2024 21:36:01 +0100 Subject: [PATCH 6/7] Can get the screenshots of VMs of a group --- .../src/controllers/groups_controller.rs | 19 ++++++++++++++++++- virtweb_backend/src/main.rs | 5 ++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/virtweb_backend/src/controllers/groups_controller.rs b/virtweb_backend/src/controllers/groups_controller.rs index bab92cc..d3cdb0d 100644 --- a/virtweb_backend/src/controllers/groups_controller.rs +++ b/virtweb_backend/src/controllers/groups_controller.rs @@ -117,13 +117,30 @@ pub async fn vm_reset(client: LibVirtReq, vms: GroupVmIdExtractor) -> HttpResult Ok(HttpResponse::Ok().json(res)) } +/// Get the screenshot of the VMs of a group +pub async fn vm_screenshot(client: LibVirtReq, vms: GroupVmIdExtractor) -> HttpResult { + if vms.0.is_empty() { + return Ok(HttpResponse::NoContent().finish()); + } + + let image = if vms.0.len() == 1 { + client.screenshot_domain(vms.0[0].uuid.unwrap()).await? + } else { + return Ok( + HttpResponse::UnprocessableEntity().json("Cannot return multiple VM screenshots!!") + ); + }; + + Ok(HttpResponse::Ok().content_type("image/png").body(image)) +} + /// Get the state of the VMs pub async fn vm_state(client: LibVirtReq, vms: GroupVmIdExtractor) -> HttpResult { let mut states = HashMap::new(); for vm in vms.0 { if let Some(uuid) = vm.uuid { - states.insert(uuid.clone(), client.get_domain_state(uuid).await?); + states.insert(uuid, client.get_domain_state(uuid).await?); } } diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index a8e18f2..c9e9765 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -240,7 +240,10 @@ async fn main() -> std::io::Result<()> { "/api/group/{gid}/vm/reset", web::get().to(groups_controller::vm_reset), ) - // TODO screenshot VM + .route( + "/api/group/{gid}/vm/screenshot", + web::get().to(groups_controller::vm_screenshot), + ) .route( "/api/group/{gid}/vm/state", web::get().to(groups_controller::vm_state), -- 2.45.2 From 1278a178f41c509d8d038649e6c68177231ca066 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 28 Nov 2024 19:03:14 +0100 Subject: [PATCH 7/7] Add groups VM configuration --- .../src/widgets/tokens/APITokenDetails.tsx | 5 + .../src/widgets/tokens/TokenRightsEditor.tsx | 134 ++++++++++++++++++ 2 files changed, 139 insertions(+) diff --git a/virtweb_frontend/src/widgets/tokens/APITokenDetails.tsx b/virtweb_frontend/src/widgets/tokens/APITokenDetails.tsx index 5a0fdd4..86f3c73 100644 --- a/virtweb_frontend/src/widgets/tokens/APITokenDetails.tsx +++ b/virtweb_frontend/src/widgets/tokens/APITokenDetails.tsx @@ -2,6 +2,7 @@ import { Button } from "@mui/material"; import Grid from "@mui/material/Grid2"; import React from "react"; import { useNavigate } from "react-router-dom"; +import { GroupApi } from "../../api/GroupApi"; import { NWFilter, NWFilterApi } from "../../api/NWFilterApi"; import { NetworkApi, NetworkInfo } from "../../api/NetworksApi"; import { ServerApi } from "../../api/ServerApi"; @@ -35,12 +36,14 @@ interface DetailsProps { export function APITokenDetails(p: DetailsProps): React.ReactElement { const [vms, setVMs] = React.useState(); + const [groups, setGroups] = React.useState(); const [networks, setNetworks] = React.useState(); const [nwFilters, setNetworkFilters] = React.useState(); const [tokens, setTokens] = React.useState(); const load = async () => { setVMs(await VMApi.GetList()); + setGroups(await GroupApi.GetList()); setNetworks(await NetworkApi.GetList()); setNetworkFilters(await NWFilterApi.GetList()); setTokens(await TokensApi.GetList()); @@ -54,6 +57,7 @@ export function APITokenDetails(p: DetailsProps): React.ReactElement { build={() => ( void; vms: VMInfo[]; + groups: string[]; networks: NetworkInfo[]; nwFilters: NWFilter[]; tokens: APIToken[]; @@ -238,6 +239,139 @@ export function TokenRightsEditor(p: { + + + + + + + Group name + Get VM info + Start VM + Shutdown VM + Suspend VM + Resume VM + Kill VM + Reset VM + Screenshot VM + Get VM State + + + + {/* All Group operations */} + + + All + + + + + + + + + + + + + {/* Per VM operations */} + {p.groups.map((v, n) => ( + + {v} + + + + + + + + + + + ))} + +
+
+ {/* Networks */}