From 09f54bf3c15fc8ed07a05b71cdc48486ba6aeefc Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Sat, 30 Nov 2024 10:59:38 +0100 Subject: [PATCH 1/6] Refactorize VM information management --- .../src/controllers/server_controller.rs | 33 +++++++++++-------- remote_backend/src/virtweb_client.rs | 2 +- remote_frontend/src/api/VMApi.ts | 18 +++++----- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/remote_backend/src/controllers/server_controller.rs b/remote_backend/src/controllers/server_controller.rs index 165174b..c66e1de 100644 --- a/remote_backend/src/controllers/server_controller.rs +++ b/remote_backend/src/controllers/server_controller.rs @@ -2,7 +2,7 @@ use crate::app_config::AppConfig; use crate::controllers::HttpResult; use crate::extractors::auth_extractor::AuthExtractor; use crate::virtweb_client; -use crate::virtweb_client::VMUuid; +use crate::virtweb_client::VMInfo; use actix_web::HttpResponse; #[derive(serde::Serialize)] @@ -20,18 +20,29 @@ pub async fn config(auth: AuthExtractor) -> HttpResult { #[derive(Default, Debug, serde::Serialize)] pub struct Rights { + groups: Vec, vms: Vec, sys_info: bool, } #[derive(Debug, serde::Serialize)] -pub struct VMInfoAndCaps { - uiid: VMUuid, +pub struct GroupInfo { name: String, - description: Option, - architecture: String, - memory: usize, - number_vcpu: usize, + vms: Vec, + can_get_state: bool, + can_start: bool, + can_shutdown: bool, + can_kill: bool, + can_reset: bool, + can_suspend: bool, + can_resume: bool, + can_screenshot: bool, +} + +#[derive(Debug, serde::Serialize)] +pub struct VMInfoAndCaps { + #[serde(flatten)] + info: VMInfo, can_get_state: bool, can_start: bool, can_shutdown: bool, @@ -46,6 +57,7 @@ pub async fn rights() -> HttpResult { let rights = virtweb_client::get_token_info().await?; let mut res = Rights { + groups: vec![], vms: vec![], sys_info: rights.can_retrieve_system_info(), }; @@ -54,12 +66,7 @@ pub async fn rights() -> HttpResult { let vm_info = virtweb_client::vm_info(v).await?; res.vms.push(VMInfoAndCaps { - uiid: vm_info.uuid, - name: vm_info.name, - description: vm_info.description.clone(), - architecture: vm_info.architecture.to_string(), - memory: vm_info.memory, - number_vcpu: vm_info.number_vcpu, + info: vm_info, can_get_state: rights.is_route_allowed("GET", &v.route_state()), can_start: rights.is_route_allowed("GET", &v.route_start()), can_shutdown: rights.is_route_allowed("GET", &v.route_shutdown()), diff --git a/remote_backend/src/virtweb_client.rs b/remote_backend/src/virtweb_client.rs index 4b5c5ba..1f5e33e 100644 --- a/remote_backend/src/virtweb_client.rs +++ b/remote_backend/src/virtweb_client.rs @@ -69,7 +69,7 @@ pub struct TokenClaims { pub nonce: String, } -#[derive(serde::Deserialize, Debug)] +#[derive(serde::Deserialize, serde::Serialize, Debug)] pub struct VMInfo { pub uuid: VMUuid, pub name: String, diff --git a/remote_frontend/src/api/VMApi.ts b/remote_frontend/src/api/VMApi.ts index 03b0c9d..16fcace 100644 --- a/remote_frontend/src/api/VMApi.ts +++ b/remote_frontend/src/api/VMApi.ts @@ -1,7 +1,7 @@ import { APIClient } from "./ApiClient"; export interface VMInfo { - uiid: string; + uuid: string; name: string; description?: string; architecture: string; @@ -34,7 +34,7 @@ export class VMApi { */ static async State(vm: VMInfo): Promise { return ( - await APIClient.exec({ method: "GET", uri: `/vm/${vm.uiid}/state` }) + await APIClient.exec({ method: "GET", uri: `/vm/${vm.uuid}/state` }) ).data.state; } @@ -42,42 +42,42 @@ export class VMApi { * Request to start VM */ static async StartVM(vm: VMInfo): Promise { - await APIClient.exec({ method: "GET", uri: `/vm/${vm.uiid}/start` }); + await APIClient.exec({ method: "GET", uri: `/vm/${vm.uuid}/start` }); } /** * Request to suspend VM */ static async SuspendVM(vm: VMInfo): Promise { - await APIClient.exec({ method: "GET", uri: `/vm/${vm.uiid}/suspend` }); + await APIClient.exec({ method: "GET", uri: `/vm/${vm.uuid}/suspend` }); } /** * Request to resume VM */ static async ResumeVM(vm: VMInfo): Promise { - await APIClient.exec({ method: "GET", uri: `/vm/${vm.uiid}/resume` }); + await APIClient.exec({ method: "GET", uri: `/vm/${vm.uuid}/resume` }); } /** * Request to shutdown VM */ static async ShutdownVM(vm: VMInfo): Promise { - await APIClient.exec({ method: "GET", uri: `/vm/${vm.uiid}/shutdown` }); + await APIClient.exec({ method: "GET", uri: `/vm/${vm.uuid}/shutdown` }); } /** * Request to kill VM */ static async KillVM(vm: VMInfo): Promise { - await APIClient.exec({ method: "GET", uri: `/vm/${vm.uiid}/kill` }); + await APIClient.exec({ method: "GET", uri: `/vm/${vm.uuid}/kill` }); } /** * Request to reset VM */ static async ResetVM(vm: VMInfo): Promise { - await APIClient.exec({ method: "GET", uri: `/vm/${vm.uiid}/reset` }); + await APIClient.exec({ method: "GET", uri: `/vm/${vm.uuid}/reset` }); } /** @@ -86,7 +86,7 @@ export class VMApi { static async Screenshot(vm: VMInfo): Promise { return ( await APIClient.exec({ - uri: `/vm/${vm.uiid}/screenshot`, + uri: `/vm/${vm.uuid}/screenshot`, method: "GET", }) ).data; -- 2.45.2 From a8a75328a9b64eebf5ea56e99a603f25753c99ea Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Sat, 30 Nov 2024 11:22:32 +0100 Subject: [PATCH 2/6] Get information about the VM of a group --- .../src/controllers/server_controller.rs | 21 ++++++++++++++-- remote_backend/src/virtweb_client.rs | 24 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/remote_backend/src/controllers/server_controller.rs b/remote_backend/src/controllers/server_controller.rs index c66e1de..38bbf81 100644 --- a/remote_backend/src/controllers/server_controller.rs +++ b/remote_backend/src/controllers/server_controller.rs @@ -2,7 +2,7 @@ use crate::app_config::AppConfig; use crate::controllers::HttpResult; use crate::extractors::auth_extractor::AuthExtractor; use crate::virtweb_client; -use crate::virtweb_client::VMInfo; +use crate::virtweb_client::{GroupID, VMInfo}; use actix_web::HttpResponse; #[derive(serde::Serialize)] @@ -27,7 +27,7 @@ pub struct Rights { #[derive(Debug, serde::Serialize)] pub struct GroupInfo { - name: String, + id: GroupID, vms: Vec, can_get_state: bool, can_start: bool, @@ -62,6 +62,23 @@ pub async fn rights() -> HttpResult { sys_info: rights.can_retrieve_system_info(), }; + for g in rights.list_groups() { + let group_vms = virtweb_client::group_vm_info(&g).await?; + + res.groups.push(GroupInfo { + id: g, + vms: group_vms, + can_get_state: false, //TODO + can_start: false, + can_shutdown: false, + can_kill: false, + can_reset: false, + can_suspend: false, + can_resume: false, + can_screenshot: false, + }) + } + for v in rights.list_vm() { let vm_info = virtweb_client::vm_info(v).await?; diff --git a/remote_backend/src/virtweb_client.rs b/remote_backend/src/virtweb_client.rs index 1f5e33e..d2e9dd9 100644 --- a/remote_backend/src/virtweb_client.rs +++ b/remote_backend/src/virtweb_client.rs @@ -12,6 +12,15 @@ pub enum VirtWebClientError { InvalidStatusCode(u16), } +#[derive(Eq, PartialEq, Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct GroupID(String); + +impl GroupID { + pub fn route_vm_info(&self) -> String { + format!("/api/group/{}/vm/info", self.0) + } +} + #[derive(Eq, PartialEq, Debug, Copy, Clone, serde::Serialize, serde::Deserialize)] pub struct VMUuid(Uuid); @@ -147,6 +156,16 @@ impl TokenInfo { false } + /// List the groups with access + pub fn list_groups(&self) -> Vec { + self.rights + .iter() + .filter(|r| r.verb == "GET") + .filter(|r| regex!("^/api/group/[^/]+/vm/info$").is_match(&r.path)) + .map(|r| GroupID(r.path.split("/").nth(3).unwrap().to_string())) + .collect::>() + } + /// List the virtual machines with access pub fn list_vm(&self) -> Vec { self.rights @@ -260,6 +279,11 @@ pub async fn vm_screenshot(id: VMUuid) -> anyhow::Result> { .to_vec()) } +/// Get the VM of a group +pub async fn group_vm_info(id: &GroupID) -> anyhow::Result> { + json_request(id.route_vm_info()).await +} + /// Get current server information pub async fn get_server_info() -> anyhow::Result { json_request("/api/server/info").await -- 2.45.2 From 26fee59c5d8000935a9715aeaa3da5437c8ccdaa Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Sat, 30 Nov 2024 15:07:12 +0100 Subject: [PATCH 3/6] Can determine the rights of the token over the group --- .../src/controllers/server_controller.rs | 18 ++--- remote_backend/src/virtweb_client.rs | 81 +++++++++++++++++++ 2 files changed, 90 insertions(+), 9 deletions(-) diff --git a/remote_backend/src/controllers/server_controller.rs b/remote_backend/src/controllers/server_controller.rs index 38bbf81..d84d51f 100644 --- a/remote_backend/src/controllers/server_controller.rs +++ b/remote_backend/src/controllers/server_controller.rs @@ -66,16 +66,16 @@ pub async fn rights() -> HttpResult { let group_vms = virtweb_client::group_vm_info(&g).await?; res.groups.push(GroupInfo { - id: g, + id: g.clone(), vms: group_vms, - can_get_state: false, //TODO - can_start: false, - can_shutdown: false, - can_kill: false, - can_reset: false, - can_suspend: false, - can_resume: false, - can_screenshot: false, + can_get_state: rights.is_route_allowed("GET", &g.route_vm_state(None)), + can_start: rights.is_route_allowed("GET", &g.route_vm_start(None)), + can_shutdown: rights.is_route_allowed("GET", &g.route_vm_shutdown(None)), + can_kill: rights.is_route_allowed("GET", &g.route_vm_kill(None)), + can_reset: rights.is_route_allowed("GET", &g.route_vm_reset(None)), + can_suspend: rights.is_route_allowed("GET", &g.route_vm_suspend(None)), + can_resume: rights.is_route_allowed("GET", &g.route_vm_resume(None)), + can_screenshot: rights.is_route_allowed("GET", &g.route_vm_screenshot(None)), }) } diff --git a/remote_backend/src/virtweb_client.rs b/remote_backend/src/virtweb_client.rs index d2e9dd9..9344bfb 100644 --- a/remote_backend/src/virtweb_client.rs +++ b/remote_backend/src/virtweb_client.rs @@ -19,6 +19,87 @@ impl GroupID { pub fn route_vm_info(&self) -> String { format!("/api/group/{}/vm/info", self.0) } + + pub fn route_vm_state(&self, vm: Option) -> String { + format!( + "/api/group/{}/vm/state{}", + self.0, + match vm { + None => "".to_string(), + Some(id) => format!("?vm_id={}", id.0), + } + ) + } + pub fn route_vm_start(&self, vm: Option) -> String { + format!( + "/api/group/{}/vm/start{}", + self.0, + match vm { + None => "".to_string(), + Some(id) => format!("?vm_id={}", id.0), + } + ) + } + pub fn route_vm_shutdown(&self, vm: Option) -> String { + format!( + "/api/group/{}/vm/shutdown{}", + self.0, + match vm { + None => "".to_string(), + Some(id) => format!("?vm_id={}", id.0), + } + ) + } + pub fn route_vm_suspend(&self, vm: Option) -> String { + format!( + "/api/group/{}/vm/suspend{}", + self.0, + match vm { + None => "".to_string(), + Some(id) => format!("?vm_id={}", id.0), + } + ) + } + pub fn route_vm_resume(&self, vm: Option) -> String { + format!( + "/api/group/{}/vm/resume{}", + self.0, + match vm { + None => "".to_string(), + Some(id) => format!("?vm_id={}", id.0), + } + ) + } + pub fn route_vm_kill(&self, vm: Option) -> String { + format!( + "/api/group/{}/vm/kill{}", + self.0, + match vm { + None => "".to_string(), + Some(id) => format!("?vm_id={}", id.0), + } + ) + } + pub fn route_vm_reset(&self, vm: Option) -> String { + format!( + "/api/group/{}/vm/reset{}", + self.0, + match vm { + None => "".to_string(), + Some(id) => format!("?vm_id={}", id.0), + } + ) + } + pub fn route_vm_screenshot(&self, vm: Option) -> String { + format!( + "/api/group/{}/vm/screenshot{}", + self.0, + match vm { + None => "".to_string(), + Some(id) => format!("?vm_id={}", id.0), + } + ) + } } #[derive(Eq, PartialEq, Debug, Copy, Clone, serde::Serialize, serde::Deserialize)] -- 2.45.2 From 269f6027b7613bf594dea6686b2549327f5e1b46 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 3 Dec 2024 21:18:05 +0100 Subject: [PATCH 4/6] Implement routes to start VMs of a group --- .../src/controllers/group_controller.rs | 30 +++++++++++++++++++ remote_backend/src/controllers/mod.rs | 1 + remote_backend/src/main.rs | 27 ++++++++++++++++- remote_backend/src/virtweb_client.rs | 28 +++++++++++++++-- 4 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 remote_backend/src/controllers/group_controller.rs diff --git a/remote_backend/src/controllers/group_controller.rs b/remote_backend/src/controllers/group_controller.rs new file mode 100644 index 0000000..7142c05 --- /dev/null +++ b/remote_backend/src/controllers/group_controller.rs @@ -0,0 +1,30 @@ +use crate::controllers::HttpResult; +use crate::virtweb_client; +use crate::virtweb_client::{GroupID, VMUuid}; +use actix_web::{web, HttpResponse}; + +#[derive(serde::Deserialize)] +pub struct GroupIDInPath { + gid: GroupID, +} + +#[derive(serde::Deserialize)] +pub struct VMIDInQuery { + vm_id: Option, +} + +/// Get the state of one or all VM +pub async fn vm_state( + path: web::Path, + query: web::Query, +) -> HttpResult { + Ok(HttpResponse::Ok().json(virtweb_client::group_vm_state(&path.gid, query.vm_id).await?)) +} + +/// Start one or all VM +pub async fn vm_start( + path: web::Path, + query: web::Query, +) -> HttpResult { + Ok(HttpResponse::Ok().json(virtweb_client::group_vm_start(&path.gid, query.vm_id).await?)) +} diff --git a/remote_backend/src/controllers/mod.rs b/remote_backend/src/controllers/mod.rs index c0d541f..02ad6b9 100644 --- a/remote_backend/src/controllers/mod.rs +++ b/remote_backend/src/controllers/mod.rs @@ -6,6 +6,7 @@ use std::fmt::{Display, Formatter}; use std::io::ErrorKind; pub mod auth_controller; +pub mod group_controller; pub mod server_controller; pub mod static_controller; pub mod sys_info_controller; diff --git a/remote_backend/src/main.rs b/remote_backend/src/main.rs index 90c0a4c..533c9c3 100644 --- a/remote_backend/src/main.rs +++ b/remote_backend/src/main.rs @@ -12,7 +12,8 @@ use light_openid::basic_state_manager::BasicStateManager; use remote_backend::app_config::AppConfig; use remote_backend::constants; use remote_backend::controllers::{ - auth_controller, server_controller, static_controller, sys_info_controller, vm_controller, + auth_controller, group_controller, server_controller, static_controller, sys_info_controller, + vm_controller, }; use remote_backend::middlewares::auth_middleware::AuthChecker; use std::time::Duration; @@ -86,6 +87,30 @@ async fn main() -> std::io::Result<()> { "/api/server/rights", web::get().to(server_controller::rights), ) + // Groups routes + .route( + "/api/group/{gid}/vm/state", + web::get().to(group_controller::vm_state), + ) + .route( + "/api/group/{gid}/vm/start", + web::get().to(group_controller::vm_start), + ) + /*.route( + "/api/group/{gid}/vm/shutdown", + web::get().to(group_controller::vm_shutdown), + ) + .route("/api/group/{gid}/vm/kill", web::get().to(group_controller::vm_kill)) + .route("/api/group/{gid}/vm/reset", web::get().to(group_controller::vm_reset)) + .route( + "/api/group/{gid}/vm/suspend", + web::get().to(group_controller::vm_suspend), + ) + .route("/api/group/{gid}/vm/resume", web::get().to(group_controller::vm_resume)) + .route( + "/api/group/{gid}/vm/screenshot", + web::get().to(group_controller::vm_screenshot), + )*/ // VM routes .route("/api/vm/{uid}/state", web::get().to(vm_controller::state)) .route("/api/vm/{uid}/start", web::get().to(vm_controller::start)) diff --git a/remote_backend/src/virtweb_client.rs b/remote_backend/src/virtweb_client.rs index 9344bfb..7e75cad 100644 --- a/remote_backend/src/virtweb_client.rs +++ b/remote_backend/src/virtweb_client.rs @@ -1,6 +1,7 @@ use crate::app_config::AppConfig; use crate::utils::time; use lazy_regex::regex; +use std::collections::HashMap; use std::fmt::Display; use std::str::FromStr; use thiserror::Error; @@ -102,9 +103,15 @@ impl GroupID { } } -#[derive(Eq, PartialEq, Debug, Copy, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Eq, PartialEq, Debug, Copy, Clone, serde::Serialize, serde::Deserialize, Hash)] pub struct VMUuid(Uuid); +#[derive(Default, serde::Deserialize, serde::Serialize)] +pub struct TreatmentResult { + ok: usize, + failed: usize, +} + impl VMUuid { pub fn route_info(&self) -> String { format!("/api/vm/{}", self.0) @@ -268,12 +275,13 @@ async fn request(uri: D) -> anyhow::Result { let url = format!("{}{}", AppConfig::get().virtweb_base_url, uri); log::debug!("Will query {uri}..."); + let uri = uri.to_string(); let jwt = TokenClaims { sub: AppConfig::get().virtweb_token_id.to_string(), iat: time() - 60 * 2, exp: time() + 60 * 3, verb: "GET".to_string(), - path: uri.to_string(), + path: uri.split_once('?').map(|s| s.0).unwrap_or(&uri).to_string(), nonce: Uuid::new_v4().to_string(), }; let jwt = AppConfig::get().token_private_key().sign_jwt(&jwt)?; @@ -365,6 +373,22 @@ pub async fn group_vm_info(id: &GroupID) -> anyhow::Result> { json_request(id.route_vm_info()).await } +/// Get the state of one or all VMs of a group +pub async fn group_vm_state( + id: &GroupID, + vm_id: Option, +) -> anyhow::Result> { + json_request(id.route_vm_state(vm_id)).await +} + +/// Start one or all VMs of a group +pub async fn group_vm_start( + id: &GroupID, + vm_id: Option, +) -> anyhow::Result { + json_request(id.route_vm_start(vm_id)).await +} + /// Get current server information pub async fn get_server_info() -> anyhow::Result { json_request("/api/server/info").await -- 2.45.2 From acb9baee23d470ebfc6d803a3f9abecca36ee8b0 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 3 Dec 2024 21:26:19 +0100 Subject: [PATCH 5/6] Implement routes to shutdown, kill, reset, suspend, resume and take screenshot of VMs of a group --- .../src/controllers/group_controller.rs | 49 +++++++++++++++++++ remote_backend/src/main.rs | 19 +++++-- remote_backend/src/virtweb_client.rs | 46 +++++++++++++++++ 3 files changed, 109 insertions(+), 5 deletions(-) diff --git a/remote_backend/src/controllers/group_controller.rs b/remote_backend/src/controllers/group_controller.rs index 7142c05..ad5deb7 100644 --- a/remote_backend/src/controllers/group_controller.rs +++ b/remote_backend/src/controllers/group_controller.rs @@ -28,3 +28,52 @@ pub async fn vm_start( ) -> HttpResult { Ok(HttpResponse::Ok().json(virtweb_client::group_vm_start(&path.gid, query.vm_id).await?)) } + +/// Shutdown one or all VM +pub async fn vm_shutdown( + path: web::Path, + query: web::Query, +) -> HttpResult { + Ok(HttpResponse::Ok().json(virtweb_client::group_vm_shutdown(&path.gid, query.vm_id).await?)) +} + +/// Kill one or all VM +pub async fn vm_kill(path: web::Path, query: web::Query) -> HttpResult { + Ok(HttpResponse::Ok().json(virtweb_client::group_vm_kill(&path.gid, query.vm_id).await?)) +} + +/// Reset one or all VM +pub async fn vm_reset( + path: web::Path, + query: web::Query, +) -> HttpResult { + Ok(HttpResponse::Ok().json(virtweb_client::group_vm_reset(&path.gid, query.vm_id).await?)) +} + +/// Suspend one or all VM +pub async fn vm_suspend( + path: web::Path, + query: web::Query, +) -> HttpResult { + Ok(HttpResponse::Ok().json(virtweb_client::group_vm_suspend(&path.gid, query.vm_id).await?)) +} + +/// Resume one or all VM +pub async fn vm_resume( + path: web::Path, + query: web::Query, +) -> HttpResult { + Ok(HttpResponse::Ok().json(virtweb_client::group_vm_resume(&path.gid, query.vm_id).await?)) +} + +/// Screenshot one or all VM +pub async fn vm_screenshot( + path: web::Path, + query: web::Query, +) -> HttpResult { + let screenshot = virtweb_client::group_vm_screenshot(&path.gid, query.vm_id).await?; + + Ok(HttpResponse::Ok() + .insert_header(("content-type", "image/png")) + .body(screenshot)) +} diff --git a/remote_backend/src/main.rs b/remote_backend/src/main.rs index 533c9c3..4febb40 100644 --- a/remote_backend/src/main.rs +++ b/remote_backend/src/main.rs @@ -96,21 +96,30 @@ async fn main() -> std::io::Result<()> { "/api/group/{gid}/vm/start", web::get().to(group_controller::vm_start), ) - /*.route( + .route( "/api/group/{gid}/vm/shutdown", web::get().to(group_controller::vm_shutdown), ) - .route("/api/group/{gid}/vm/kill", web::get().to(group_controller::vm_kill)) - .route("/api/group/{gid}/vm/reset", web::get().to(group_controller::vm_reset)) + .route( + "/api/group/{gid}/vm/kill", + web::get().to(group_controller::vm_kill), + ) + .route( + "/api/group/{gid}/vm/reset", + web::get().to(group_controller::vm_reset), + ) .route( "/api/group/{gid}/vm/suspend", web::get().to(group_controller::vm_suspend), ) - .route("/api/group/{gid}/vm/resume", web::get().to(group_controller::vm_resume)) + .route( + "/api/group/{gid}/vm/resume", + web::get().to(group_controller::vm_resume), + ) .route( "/api/group/{gid}/vm/screenshot", web::get().to(group_controller::vm_screenshot), - )*/ + ) // VM routes .route("/api/vm/{uid}/state", web::get().to(vm_controller::state)) .route("/api/vm/{uid}/start", web::get().to(vm_controller::start)) diff --git a/remote_backend/src/virtweb_client.rs b/remote_backend/src/virtweb_client.rs index 7e75cad..9821ad4 100644 --- a/remote_backend/src/virtweb_client.rs +++ b/remote_backend/src/virtweb_client.rs @@ -389,6 +389,52 @@ pub async fn group_vm_start( json_request(id.route_vm_start(vm_id)).await } +/// Shutdown one or all VMs of a group +pub async fn group_vm_shutdown( + id: &GroupID, + vm_id: Option, +) -> anyhow::Result { + json_request(id.route_vm_shutdown(vm_id)).await +} + +/// Kill one or all VMs of a group +pub async fn group_vm_kill(id: &GroupID, vm_id: Option) -> anyhow::Result { + json_request(id.route_vm_kill(vm_id)).await +} + +/// Reset one or all VMs of a group +pub async fn group_vm_reset( + id: &GroupID, + vm_id: Option, +) -> anyhow::Result { + json_request(id.route_vm_reset(vm_id)).await +} + +/// Suspend one or all VMs of a group +pub async fn group_vm_suspend( + id: &GroupID, + vm_id: Option, +) -> anyhow::Result { + json_request(id.route_vm_suspend(vm_id)).await +} + +/// Resume one or all VMs of a group +pub async fn group_vm_resume( + id: &GroupID, + vm_id: Option, +) -> anyhow::Result { + json_request(id.route_vm_resume(vm_id)).await +} + +/// Get the screenshot of one or all VMs of a group +pub async fn group_vm_screenshot(id: &GroupID, vm_id: Option) -> anyhow::Result> { + Ok(request(id.route_vm_screenshot(vm_id)) + .await? + .bytes() + .await? + .to_vec()) +} + /// Get current server information pub async fn get_server_info() -> anyhow::Result { json_request("/api/server/info").await -- 2.45.2 From d243022810e1bb67365f15c0480bdbf21f10e012 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 3 Dec 2024 21:49:53 +0100 Subject: [PATCH 6/6] Ready to implement groups web ui integration --- .../src/controllers/server_controller.rs | 58 ++++++++----------- remote_backend/src/virtweb_client.rs | 12 ++++ remote_frontend/src/App.tsx | 15 ++++- remote_frontend/src/api/ServerApi.ts | 12 +++- remote_frontend/src/api/VMApi.ts | 5 ++ remote_frontend/src/widgets/GroupsWidget.tsx | 5 ++ .../src/widgets/VirtualMachinesWidget.tsx | 9 ++- 7 files changed, 77 insertions(+), 39 deletions(-) create mode 100644 remote_frontend/src/widgets/GroupsWidget.tsx diff --git a/remote_backend/src/controllers/server_controller.rs b/remote_backend/src/controllers/server_controller.rs index d84d51f..962dc05 100644 --- a/remote_backend/src/controllers/server_controller.rs +++ b/remote_backend/src/controllers/server_controller.rs @@ -2,7 +2,7 @@ use crate::app_config::AppConfig; use crate::controllers::HttpResult; use crate::extractors::auth_extractor::AuthExtractor; use crate::virtweb_client; -use crate::virtweb_client::{GroupID, VMInfo}; +use crate::virtweb_client::{GroupID, VMCaps, VMInfo}; use actix_web::HttpResponse; #[derive(serde::Serialize)] @@ -29,28 +29,16 @@ pub struct Rights { pub struct GroupInfo { id: GroupID, vms: Vec, - can_get_state: bool, - can_start: bool, - can_shutdown: bool, - can_kill: bool, - can_reset: bool, - can_suspend: bool, - can_resume: bool, - can_screenshot: bool, + #[serde(flatten)] + caps: VMCaps, } #[derive(Debug, serde::Serialize)] pub struct VMInfoAndCaps { #[serde(flatten)] info: VMInfo, - can_get_state: bool, - can_start: bool, - can_shutdown: bool, - can_kill: bool, - can_reset: bool, - can_suspend: bool, - can_resume: bool, - can_screenshot: bool, + #[serde(flatten)] + caps: VMCaps, } pub async fn rights() -> HttpResult { @@ -68,14 +56,16 @@ pub async fn rights() -> HttpResult { res.groups.push(GroupInfo { id: g.clone(), vms: group_vms, - can_get_state: rights.is_route_allowed("GET", &g.route_vm_state(None)), - can_start: rights.is_route_allowed("GET", &g.route_vm_start(None)), - can_shutdown: rights.is_route_allowed("GET", &g.route_vm_shutdown(None)), - can_kill: rights.is_route_allowed("GET", &g.route_vm_kill(None)), - can_reset: rights.is_route_allowed("GET", &g.route_vm_reset(None)), - can_suspend: rights.is_route_allowed("GET", &g.route_vm_suspend(None)), - can_resume: rights.is_route_allowed("GET", &g.route_vm_resume(None)), - can_screenshot: rights.is_route_allowed("GET", &g.route_vm_screenshot(None)), + caps: VMCaps { + can_get_state: rights.is_route_allowed("GET", &g.route_vm_state(None)), + can_start: rights.is_route_allowed("GET", &g.route_vm_start(None)), + can_shutdown: rights.is_route_allowed("GET", &g.route_vm_shutdown(None)), + can_kill: rights.is_route_allowed("GET", &g.route_vm_kill(None)), + can_reset: rights.is_route_allowed("GET", &g.route_vm_reset(None)), + can_suspend: rights.is_route_allowed("GET", &g.route_vm_suspend(None)), + can_resume: rights.is_route_allowed("GET", &g.route_vm_resume(None)), + can_screenshot: rights.is_route_allowed("GET", &g.route_vm_screenshot(None)), + }, }) } @@ -84,14 +74,16 @@ pub async fn rights() -> HttpResult { res.vms.push(VMInfoAndCaps { info: vm_info, - can_get_state: rights.is_route_allowed("GET", &v.route_state()), - can_start: rights.is_route_allowed("GET", &v.route_start()), - can_shutdown: rights.is_route_allowed("GET", &v.route_shutdown()), - can_kill: rights.is_route_allowed("GET", &v.route_kill()), - can_reset: rights.is_route_allowed("GET", &v.route_reset()), - can_suspend: rights.is_route_allowed("GET", &v.route_suspend()), - can_resume: rights.is_route_allowed("GET", &v.route_resume()), - can_screenshot: rights.is_route_allowed("GET", &v.route_screenshot()), + caps: VMCaps { + can_get_state: rights.is_route_allowed("GET", &v.route_state()), + can_start: rights.is_route_allowed("GET", &v.route_start()), + can_shutdown: rights.is_route_allowed("GET", &v.route_shutdown()), + can_kill: rights.is_route_allowed("GET", &v.route_kill()), + can_reset: rights.is_route_allowed("GET", &v.route_reset()), + can_suspend: rights.is_route_allowed("GET", &v.route_suspend()), + can_resume: rights.is_route_allowed("GET", &v.route_resume()), + can_screenshot: rights.is_route_allowed("GET", &v.route_screenshot()), + }, }) } diff --git a/remote_backend/src/virtweb_client.rs b/remote_backend/src/virtweb_client.rs index 9821ad4..10748f9 100644 --- a/remote_backend/src/virtweb_client.rs +++ b/remote_backend/src/virtweb_client.rs @@ -176,6 +176,18 @@ pub struct VMInfo { pub number_vcpu: usize, } +#[derive(serde::Deserialize, serde::Serialize, Debug)] +pub struct VMCaps { + pub can_get_state: bool, + pub can_start: bool, + pub can_shutdown: bool, + pub can_kill: bool, + pub can_reset: bool, + pub can_suspend: bool, + pub can_resume: bool, + pub can_screenshot: bool, +} + #[derive(serde::Deserialize, serde::Serialize, Debug)] pub struct VMState { pub state: String, diff --git a/remote_frontend/src/App.tsx b/remote_frontend/src/App.tsx index c65fc8f..a8951d7 100644 --- a/remote_frontend/src/App.tsx +++ b/remote_frontend/src/App.tsx @@ -5,6 +5,8 @@ import { typographyStyles, } from "@fluentui/react-components"; import { + AppsListDetailFilled, + AppsListDetailRegular, DesktopFilled, DesktopRegular, InfoFilled, @@ -18,6 +20,7 @@ import { AsyncWidget } from "./widgets/AsyncWidget"; import { MainMenu } from "./widgets/MainMenu"; import { SystemInfoWidget } from "./widgets/SystemInfoWidget"; import { VirtualMachinesWidget } from "./widgets/VirtualMachinesWidget"; +import { GroupsWidget } from "./widgets/GroupsWidget"; const useStyles = makeStyles({ title: typographyStyles.title2, @@ -27,6 +30,8 @@ const InfoIcon = bundleIcon(InfoFilled, InfoRegular); const DesktopIcon = bundleIcon(DesktopFilled, DesktopRegular); +const AppListIcon = bundleIcon(AppsListDetailFilled, AppsListDetailRegular); + export function App() { return ( ("vm"); + const [tab, setTab] = React.useState<"group" | "vm" | "info">("group"); const [rights, setRights] = React.useState(); @@ -82,6 +87,13 @@ function AuthenticatedApp(): React.ReactElement { selectedValue={tab} onTabSelect={(_, d) => setTab(d.value as any)} > + } + disabled={rights!.groups.length === 0} + > + Groups + } @@ -101,6 +113,7 @@ function AuthenticatedApp(): React.ReactElement { + {tab === "group" && } {tab === "vm" && } {tab === "info" && } diff --git a/remote_frontend/src/api/ServerApi.ts b/remote_frontend/src/api/ServerApi.ts index ad6b441..7c5abb6 100644 --- a/remote_frontend/src/api/ServerApi.ts +++ b/remote_frontend/src/api/ServerApi.ts @@ -1,5 +1,5 @@ import { APIClient } from "./ApiClient"; -import { VMInfo } from "./VMApi"; +import { VMCaps, VMInfo, VMInfoAndCaps } from "./VMApi"; export interface ServerConfig { authenticated: boolean; @@ -7,10 +7,18 @@ export interface ServerConfig { } export interface Rights { - vms: VMInfo[]; + groups: VMGroup[]; + vms: VMInfoAndCaps[]; sys_info: boolean; } +export type VMGroup = VMGroupInfo & VMCaps; + +export interface VMGroupInfo { + id: string; + vms: VMInfo[]; +} + let config: ServerConfig | null = null; export class ServerApi { diff --git a/remote_frontend/src/api/VMApi.ts b/remote_frontend/src/api/VMApi.ts index 16fcace..42902be 100644 --- a/remote_frontend/src/api/VMApi.ts +++ b/remote_frontend/src/api/VMApi.ts @@ -7,6 +7,9 @@ export interface VMInfo { architecture: string; memory: number; number_vcpu: number; +} + +export interface VMCaps { can_get_state: boolean; can_start: boolean; can_shutdown: boolean; @@ -17,6 +20,8 @@ export interface VMInfo { can_screenshot: boolean; } +export type VMInfoAndCaps = VMInfo & VMCaps; + export type VMState = | "NoState" | "Running" diff --git a/remote_frontend/src/widgets/GroupsWidget.tsx b/remote_frontend/src/widgets/GroupsWidget.tsx new file mode 100644 index 0000000..4f553e7 --- /dev/null +++ b/remote_frontend/src/widgets/GroupsWidget.tsx @@ -0,0 +1,5 @@ +import { Rights } from "../api/ServerApi"; + +export function GroupsWidget(p: { rights: Rights }): React.ReactElement { + return

TODO

; +} diff --git a/remote_frontend/src/widgets/VirtualMachinesWidget.tsx b/remote_frontend/src/widgets/VirtualMachinesWidget.tsx index 7fb047d..b0c78ca 100644 --- a/remote_frontend/src/widgets/VirtualMachinesWidget.tsx +++ b/remote_frontend/src/widgets/VirtualMachinesWidget.tsx @@ -22,7 +22,7 @@ import { import { filesize } from "filesize"; import React from "react"; import { Rights } from "../api/ServerApi"; -import { VMApi, VMInfo, VMState } from "../api/VMApi"; +import { VMApi, VMInfo, VMInfoAndCaps, VMState } from "../api/VMApi"; import { useConfirm } from "../hooks/providers/ConfirmDialogProvider"; import { useToast } from "../hooks/providers/ToastProvider"; import { VMLiveScreenshot } from "./VMLiveScreenshot"; @@ -54,7 +54,7 @@ export function VirtualMachinesWidget(p: { ); } -function VMWidget(p: { vm: VMInfo }): React.ReactElement { +function VMWidget(p: { vm: VMInfoAndCaps }): React.ReactElement { const toast = useToast(); const [state, setState] = React.useState(); @@ -189,7 +189,10 @@ function VMWidget(p: { vm: VMInfo }): React.ReactElement { ); } -function VMPreview(p: { vm: VMInfo; state?: VMState }): React.ReactElement { +function VMPreview(p: { + vm: VMInfoAndCaps; + state?: VMState; +}): React.ReactElement { const styles = useStyles(); if (!p.vm.can_screenshot || p.state !== "Running") { return ( -- 2.45.2