From 57c023b45b80002dbf2b394e49ab7a19f5618fe3 Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Wed, 6 Sep 2023 18:54:38 +0200 Subject: [PATCH] Can query hypervisor information --- README.md | 23 ++++++ virtweb_backend/Cargo.lock | 77 ++++++++++++++++++ virtweb_backend/Cargo.toml | 4 +- virtweb_backend/src/actors/libvirt_actor.rs | 79 +++++++++++++++++++ virtweb_backend/src/actors/mod.rs | 1 + virtweb_backend/src/app_config.rs | 4 + virtweb_backend/src/controllers/mod.rs | 5 +- .../src/controllers/server_controller.rs | 13 +++ virtweb_backend/src/lib.rs | 2 + virtweb_backend/src/libvirt_client.rs | 13 +++ virtweb_backend/src/main.rs | 15 ++++ virtweb_frontend/src/api/ServerApi.ts | 14 ++++ .../src/routes/auth/LoginRoute.tsx | 3 +- .../src/widgets/BaseAuthenticatedPage.tsx | 9 +-- 14 files changed, 253 insertions(+), 9 deletions(-) create mode 100644 virtweb_backend/src/actors/libvirt_actor.rs create mode 100644 virtweb_backend/src/actors/mod.rs create mode 100644 virtweb_backend/src/libvirt_client.rs diff --git a/README.md b/README.md index e3a8b88..a09ddf0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,25 @@ # VirtWEB WIP project + +## Development requirements +1. The `libvirt-dev` package must be installed: + +```bash +sudo apt install libvirt-dev +``` + +2. Libvirt must also be installed: +```bash +sudo apt install qemu-kvm libvirt-daemon-system +``` + +3. Allow the current user to manage VMs: +``` +sudo adduser $USER libvirt +``` + +> Note: You will need to login again for this change to take effect. + + +## Production requirements +TODO diff --git a/virtweb_backend/Cargo.lock b/virtweb_backend/Cargo.lock index 73e1995..888b25e 100644 --- a/virtweb_backend/Cargo.lock +++ b/virtweb_backend/Cargo.lock @@ -2,6 +2,31 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "actix" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cba56612922b907719d4a01cf11c8d5b458e7d3dba946d0435f20f58d6795ed2" +dependencies = [ + "actix-macros", + "actix-rt", + "actix_derive", + "bitflags 2.4.0", + "bytes", + "crossbeam-channel", + "futures-core", + "futures-sink", + "futures-task", + "futures-util", + "log", + "once_cell", + "parking_lot", + "pin-project-lite", + "smallvec", + "tokio", + "tokio-util", +] + [[package]] name = "actix-codec" version = "0.5.1" @@ -301,6 +326,17 @@ dependencies = [ "syn 2.0.29", ] +[[package]] +name = "actix_derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d44b8fee1ced9671ba043476deddef739dd0959bf77030b26b738cc591737a7" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "addr2line" version = "0.21.0" @@ -703,6 +739,25 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -2042,6 +2097,26 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "virt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9316a0df71f1ec209e7ef8ab07097e4181945b245d3348f57de07a3e811e53cf" +dependencies = [ + "libc", + "virt-sys", +] + +[[package]] +name = "virt-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd39f6e0d0ab3fe7c371fac05c9b6ca72e186f06a2e666fb3b95441091eba2db" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "virtue" version = "0.0.13" @@ -2052,6 +2127,7 @@ checksum = "9dcc60c0624df774c82a0ef104151231d37da4962957d691c011c852b2473314" name = "virtweb_backend" version = "0.1.0" dependencies = [ + "actix", "actix-cors", "actix-files", "actix-identity", @@ -2071,6 +2147,7 @@ dependencies = [ "serde_json", "tempfile", "url", + "virt", ] [[package]] diff --git a/virtweb_backend/Cargo.toml b/virtweb_backend/Cargo.toml index 8078cc6..083d145 100644 --- a/virtweb_backend/Cargo.toml +++ b/virtweb_backend/Cargo.toml @@ -11,6 +11,7 @@ env_logger = "0.10.0" clap = { version = "4.3.19", features = ["derive", "env"] } light-openid = { version = "1.0.1", features = ["crypto-wrapper"] } lazy_static = "1.4.0" +actix = "0.13.1" actix-web = "4" actix-remote-ip = "0.1.0" actix-session = { version = "0.7.2", features = ["cookie-session"] } @@ -24,4 +25,5 @@ anyhow = "1.0.75" actix-multipart = "0.6.1" tempfile = "3.8.0" reqwest = { version = "0.11.18", features = ["stream"] } -url = "2.4.0" \ No newline at end of file +url = "2.4.0" +virt = "0.3.0" \ No newline at end of file diff --git a/virtweb_backend/src/actors/libvirt_actor.rs b/virtweb_backend/src/actors/libvirt_actor.rs new file mode 100644 index 0000000..ae29d50 --- /dev/null +++ b/virtweb_backend/src/actors/libvirt_actor.rs @@ -0,0 +1,79 @@ +use crate::app_config::AppConfig; +use actix::{Actor, Context, Handler, Message}; +use virt::connect::Connect; + +pub struct LibVirtActor { + m: Connect, +} + +impl LibVirtActor { + /// Connect to hypervisor + pub async fn connect() -> anyhow::Result { + let hypervisor_uri = AppConfig::get().hypervisor_uri.as_deref().unwrap_or(""); + log::info!( + "Will connect to hypvervisor at address '{}'", + hypervisor_uri + ); + let conn = Connect::open(hypervisor_uri)?; + + Ok(Self { m: conn }) + } +} + +impl Actor for LibVirtActor { + type Context = Context; +} + +#[derive(Message)] +#[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_sockets: u32, +} + +impl Handler for LibVirtActor { + type Result = anyhow::Result; + + fn handle(&mut self, _msg: GetHypervisorInfo, _ctx: &mut Self::Context) -> Self::Result { + let node = self.m.get_node_info()?; + Ok(HypervisorInfo { + r#type: self.m.get_type()?, + hyp_version: self.m.get_hyp_version()?, + lib_version: self.m.get_lib_version()?, + capabilities: self.m.get_capabilities()?, + free_memory: self.m.get_free_memory()?, + hostname: self.m.get_hostname()?, + node: HypervisorNodeInfo { + cpu_model: node.model, + memory_size: node.memory, + number_of_active_cpus: node.cpus, + cpu_frequency_mhz: node.mhz, + number_of_numa_cell: node.nodes, + number_of_cpu_socket_per_node: node.sockets, + number_of_core_per_sockets: node.cores, + number_of_threads_per_sockets: node.threads, + }, + }) + } +} diff --git a/virtweb_backend/src/actors/mod.rs b/virtweb_backend/src/actors/mod.rs new file mode 100644 index 0000000..5e9ba0b --- /dev/null +++ b/virtweb_backend/src/actors/mod.rs @@ -0,0 +1 @@ +pub mod libvirt_actor; diff --git a/virtweb_backend/src/app_config.rs b/virtweb_backend/src/app_config.rs index 5e37c0b..7917342 100644 --- a/virtweb_backend/src/app_config.rs +++ b/virtweb_backend/src/app_config.rs @@ -72,6 +72,10 @@ pub struct AppConfig { /// Directory where temporary files are stored #[arg(long, env, default_value = "/tmp")] pub temp_dir: String, + + /// Hypervisor URI. If not specified, "" will be used instead + #[arg(long, env)] + pub hypervisor_uri: Option, } lazy_static::lazy_static! { diff --git a/virtweb_backend/src/controllers/mod.rs b/virtweb_backend/src/controllers/mod.rs index 635fb39..5233a99 100644 --- a/virtweb_backend/src/controllers/mod.rs +++ b/virtweb_backend/src/controllers/mod.rs @@ -1,5 +1,6 @@ +use crate::libvirt_client::LibVirtClient; use actix_web::body::BoxBody; -use actix_web::HttpResponse; +use actix_web::{web, HttpResponse}; use std::error::Error; use std::fmt::{Display, Formatter}; use std::io::ErrorKind; @@ -78,3 +79,5 @@ impl From for HttpErr { } pub type HttpResult = Result; + +pub type LibVirtReq = web::Data; diff --git a/virtweb_backend/src/controllers/server_controller.rs b/virtweb_backend/src/controllers/server_controller.rs index cd13e23..936d2ec 100644 --- a/virtweb_backend/src/controllers/server_controller.rs +++ b/virtweb_backend/src/controllers/server_controller.rs @@ -1,5 +1,7 @@ +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 actix_web::{HttpResponse, Responder}; @@ -23,3 +25,14 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder { iso_max_size: constants::ISO_MAX_SIZE, }) } + +#[derive(serde::Serialize)] +struct ServerInfo { + hypervisor: HypervisorInfo, +} + +pub async fn server_info(client: LibVirtReq) -> HttpResult { + Ok(HttpResponse::Ok().json(ServerInfo { + hypervisor: client.get_info().await?, + })) +} diff --git a/virtweb_backend/src/lib.rs b/virtweb_backend/src/lib.rs index 38afb55..51ef95d 100644 --- a/virtweb_backend/src/lib.rs +++ b/virtweb_backend/src/lib.rs @@ -1,6 +1,8 @@ +pub mod actors; pub mod app_config; pub mod constants; pub mod controllers; pub mod extractors; +pub mod libvirt_client; pub mod middlewares; pub mod utils; diff --git a/virtweb_backend/src/libvirt_client.rs b/virtweb_backend/src/libvirt_client.rs new file mode 100644 index 0000000..ededcdc --- /dev/null +++ b/virtweb_backend/src/libvirt_client.rs @@ -0,0 +1,13 @@ +use crate::actors::libvirt_actor; +use crate::actors::libvirt_actor::{HypervisorInfo, LibVirtActor}; +use actix::Addr; + +#[derive(Clone)] +pub struct LibVirtClient(pub Addr); + +impl LibVirtClient { + /// Get hypervisor info + pub async fn get_info(&self) -> anyhow::Result { + self.0.send(libvirt_actor::GetHypervisorInfo).await? + } +} diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index b11be7a..00f8f46 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -1,3 +1,4 @@ +use actix::Actor; use actix_cors::Cors; use actix_identity::config::LogoutBehaviour; use actix_identity::IdentityMiddleware; @@ -13,12 +14,14 @@ use actix_web::web::Data; use actix_web::{web, App, HttpServer}; use light_openid::basic_state_manager::BasicStateManager; use std::time::Duration; +use virtweb_backend::actors::libvirt_actor::LibVirtActor; use virtweb_backend::app_config::AppConfig; 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::libvirt_client::LibVirtClient; use virtweb_backend::middlewares::auth_middleware::AuthChecker; use virtweb_backend::utils::files_utils; @@ -29,6 +32,13 @@ async fn main() -> std::io::Result<()> { log::debug!("Create required directory, if missing"); files_utils::create_directory_if_missing(&AppConfig::get().iso_storage_path()).unwrap(); + let conn = Data::new(LibVirtClient( + LibVirtActor::connect() + .await + .expect("Failed to connect to hypervisor!") + .start(), + )); + log::info!("Start to listen on {}", AppConfig::get().listen_address); let state_manager = Data::new(BasicStateManager::new()); @@ -69,6 +79,7 @@ async fn main() -> std::io::Result<()> { .app_data(Data::new(RemoteIPConfig { proxy: AppConfig::get().proxy_ip.clone(), })) + .app_data(conn.clone()) // Uploaded files .app_data(MultipartFormConfig::default().total_limit(constants::ISO_MAX_SIZE)) .app_data(TempFileConfig::default().directory(&AppConfig::get().temp_dir)) @@ -78,6 +89,10 @@ async fn main() -> std::io::Result<()> { "/api/server/static_config", web::get().to(server_controller::static_config), ) + .route( + "/api/server/info", + web::get().to(server_controller::server_info), + ) // Auth controller .route( "/api/auth/local", diff --git a/virtweb_frontend/src/api/ServerApi.ts b/virtweb_frontend/src/api/ServerApi.ts index 06eb01e..7c0060b 100644 --- a/virtweb_frontend/src/api/ServerApi.ts +++ b/virtweb_frontend/src/api/ServerApi.ts @@ -9,6 +9,8 @@ export interface ServerConfig { let config: ServerConfig | null = null; +export interface ServerInfo {} + export class ServerApi { /** * Get server configuration @@ -29,4 +31,16 @@ export class ServerApi { if (config === null) throw new Error("Missing configuration!"); return config; } + + /** + * Get server information + */ + static async SystemInfo(): Promise { + return ( + await APIClient.exec({ + method: "GET", + uri: "/server/info", + }) + ).data; + } } diff --git a/virtweb_frontend/src/routes/auth/LoginRoute.tsx b/virtweb_frontend/src/routes/auth/LoginRoute.tsx index b38d1b9..44868e0 100644 --- a/virtweb_frontend/src/routes/auth/LoginRoute.tsx +++ b/virtweb_frontend/src/routes/auth/LoginRoute.tsx @@ -11,11 +11,10 @@ import { } from "@mui/material"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; -import Grid from "@mui/material/Grid"; import TextField from "@mui/material/TextField"; import Typography from "@mui/material/Typography"; import * as React from "react"; -import { Link, useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { useAuth } from "../../App"; import { AuthApi } from "../../api/AuthApi"; import { ServerApi } from "../../api/ServerApi"; diff --git a/virtweb_frontend/src/widgets/BaseAuthenticatedPage.tsx b/virtweb_frontend/src/widgets/BaseAuthenticatedPage.tsx index 3be6e48..6ea6ab7 100644 --- a/virtweb_frontend/src/widgets/BaseAuthenticatedPage.tsx +++ b/virtweb_frontend/src/widgets/BaseAuthenticatedPage.tsx @@ -1,3 +1,5 @@ +import { mdiDisc, mdiHome, mdiLanPending } from "@mdi/js"; +import Icon from "@mdi/react"; import { Box, List, @@ -5,13 +7,10 @@ import { ListItemIcon, ListItemSecondaryAction, ListItemText, - ListSubheader, } from "@mui/material"; -import { VirtWebAppBar } from "./VirtWebAppBar"; -import { RouterLink } from "./RouterLink"; import { Outlet, useLocation } from "react-router-dom"; -import Icon from "@mdi/react"; -import { mdiDisc, mdiHome, mdiLanPending } from "@mdi/js"; +import { RouterLink } from "./RouterLink"; +import { VirtWebAppBar } from "./VirtWebAppBar"; export function BaseAuthenticatedPage(): React.ReactElement { return (