From 4d0b4929c51560cc34c9c0d9c935bf3a86a44f7e Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 11 Jan 2024 19:02:47 +0100 Subject: [PATCH] Show guidelines on UI on how to setup network hook --- virtweb_backend/src/constants.rs | 3 + .../src/controllers/server_controller.rs | 16 +++ virtweb_backend/src/main.rs | 4 + virtweb_backend/src/nat/mod.rs | 1 + virtweb_backend/src/nat/nat_hook.rs | 29 +++++ virtweb_frontend/src/api/ServerApi.ts | 18 ++++ .../src/routes/NetworksListRoute.tsx | 3 + .../src/widgets/CopyToClipboard.tsx | 30 ++++++ .../widgets/net/NetworkHookStatusWidget.tsx | 102 ++++++++++++++++++ 9 files changed, 206 insertions(+) create mode 100644 virtweb_backend/src/nat/nat_hook.rs create mode 100644 virtweb_frontend/src/widgets/CopyToClipboard.tsx create mode 100644 virtweb_frontend/src/widgets/net/NetworkHookStatusWidget.tsx diff --git a/virtweb_backend/src/constants.rs b/virtweb_backend/src/constants.rs index f026cc8..7942fcf 100644 --- a/virtweb_backend/src/constants.rs +++ b/virtweb_backend/src/constants.rs @@ -86,3 +86,6 @@ pub const STORAGE_NAT_DIR: &str = "nat"; /// Environment variable that is set to run VirtWeb in NAT configuration mode pub const NAT_MODE_ENV_VAR_NAME: &str = "NAT_MODE"; + +/// Nat hook file path +pub const NAT_HOOK_PATH: &str = "/etc/libvirt/hooks/network"; diff --git a/virtweb_backend/src/controllers/server_controller.rs b/virtweb_backend/src/controllers/server_controller.rs index 9c71db7..86ab0fe 100644 --- a/virtweb_backend/src/controllers/server_controller.rs +++ b/virtweb_backend/src/controllers/server_controller.rs @@ -5,6 +5,7 @@ use crate::constants::{DISK_NAME_MAX_LEN, DISK_NAME_MIN_LEN, DISK_SIZE_MAX, DISK use crate::controllers::{HttpResult, LibVirtReq}; use crate::extractors::local_auth_extractor::LocalAuthEnabled; use crate::libvirt_rest_structures::hypervisor::HypervisorInfo; +use crate::nat::nat_hook; use crate::utils::net_utils; use actix_web::{HttpResponse, Responder}; use sysinfo::{System, SystemExt}; @@ -120,6 +121,21 @@ pub async fn server_info(client: LibVirtReq) -> HttpResult { })) } +#[derive(serde::Serialize)] +struct NetworkHookStatus { + installed: bool, + content: String, + path: &'static str, +} + +pub async fn network_hook_status() -> HttpResult { + Ok(HttpResponse::Ok().json(NetworkHookStatus { + installed: nat_hook::is_installed()?, + content: nat_hook::hook_content()?, + path: constants::NAT_HOOK_PATH, + })) +} + pub async fn number_vcpus() -> HttpResult { let mut system = System::new(); system.refresh_cpu(); diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index 5d940b0..d05a110 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -118,6 +118,10 @@ async fn main() -> std::io::Result<()> { "/api/server/info", web::get().to(server_controller::server_info), ) + .route( + "/api/server/network_hook_status", + web::get().to(server_controller::network_hook_status), + ) .route( "/api/server/number_vcpus", web::get().to(server_controller::number_vcpus), diff --git a/virtweb_backend/src/nat/mod.rs b/virtweb_backend/src/nat/mod.rs index d604f47..95b9a48 100644 --- a/virtweb_backend/src/nat/mod.rs +++ b/virtweb_backend/src/nat/mod.rs @@ -1,3 +1,4 @@ pub mod nat_conf_mode; pub mod nat_definition; +pub mod nat_hook; pub mod nat_lib; diff --git a/virtweb_backend/src/nat/nat_hook.rs b/virtweb_backend/src/nat/nat_hook.rs new file mode 100644 index 0000000..78903b7 --- /dev/null +++ b/virtweb_backend/src/nat/nat_hook.rs @@ -0,0 +1,29 @@ +use crate::app_config::AppConfig; +use crate::constants; +use std::path::Path; + +/// Check out whether NAT hook has been installed or not +pub fn is_installed() -> anyhow::Result { + let hook_file = Path::new(constants::NAT_HOOK_PATH); + + if !hook_file.exists() { + return Ok(false); + } + + let exe = std::env::current_exe()?; + let hook_content = std::fs::read_to_string(hook_file)?; + + Ok(hook_content.contains(exe.to_string_lossy().as_ref())) +} + +/// Get nat hook expected content +pub fn hook_content() -> anyhow::Result { + let exe = std::env::current_exe()?; + + Ok(format!( + "#!/bin/bash\n\ + {} --storage {} --network-name \"$1\" --operation \"$2\" --sub-operation \"$3\"", + exe.to_string_lossy(), + AppConfig::get().storage + )) +} diff --git a/virtweb_frontend/src/api/ServerApi.ts b/virtweb_frontend/src/api/ServerApi.ts index 8dda79d..de384ba 100644 --- a/virtweb_frontend/src/api/ServerApi.ts +++ b/virtweb_frontend/src/api/ServerApi.ts @@ -148,6 +148,12 @@ interface SysLoadAverage { fifteen: number; } +export interface NetworkHookStatus { + installed: boolean; + content: string; + path: string; +} + export class ServerApi { /** * Get server configuration @@ -181,6 +187,18 @@ export class ServerApi { ).data; } + /** + * Get network hook status + */ + static async NetworkHookStatus(): Promise { + return ( + await APIClient.exec({ + method: "GET", + uri: "/server/network_hook_status", + }) + ).data; + } + /** * Get host supported vCPUs configurations */ diff --git a/virtweb_frontend/src/routes/NetworksListRoute.tsx b/virtweb_frontend/src/routes/NetworksListRoute.tsx index 0904049..8f7c5d3 100644 --- a/virtweb_frontend/src/routes/NetworksListRoute.tsx +++ b/virtweb_frontend/src/routes/NetworksListRoute.tsx @@ -19,6 +19,7 @@ import { RouterLink } from "../widgets/RouterLink"; import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; import { NetworkStatusWidget } from "../widgets/net/NetworkStatusWidget"; import { useNavigate } from "react-router-dom"; +import { NetworkHookStatusWidget } from "../widgets/net/NetworkHookStatusWidget"; export function NetworksListRoute(): React.ReactElement { const [list, setList] = React.useState(); @@ -54,6 +55,8 @@ function NetworksListRouteInner(p: { } > + + diff --git a/virtweb_frontend/src/widgets/CopyToClipboard.tsx b/virtweb_frontend/src/widgets/CopyToClipboard.tsx new file mode 100644 index 0000000..231cb23 --- /dev/null +++ b/virtweb_frontend/src/widgets/CopyToClipboard.tsx @@ -0,0 +1,30 @@ +import { ButtonBase } from "@mui/material"; +import { PropsWithChildren } from "react"; +import { useSnackbar } from "../hooks/providers/SnackbarProvider"; + +export function CopyToClipboard( + p: PropsWithChildren<{ content: string }> +): React.ReactElement { + const snackbar = useSnackbar(); + + const copy = () => { + navigator.clipboard.writeText(p.content); + snackbar(`${p.content} copied to clipboard.`); + }; + + return ( + + {p.children} + + ); +} diff --git a/virtweb_frontend/src/widgets/net/NetworkHookStatusWidget.tsx b/virtweb_frontend/src/widgets/net/NetworkHookStatusWidget.tsx new file mode 100644 index 0000000..20d1430 --- /dev/null +++ b/virtweb_frontend/src/widgets/net/NetworkHookStatusWidget.tsx @@ -0,0 +1,102 @@ +import React, { PropsWithChildren } from "react"; +import { AsyncWidget } from "../AsyncWidget"; +import { NetworkHookStatus, ServerApi } from "../../api/ServerApi"; +import { Alert, Typography } from "@mui/material"; +import { CopyToClipboard } from "../CopyToClipboard"; + +export function NetworkHookStatusWidget(p: { + hiddenIfInstalled: boolean; +}): React.ReactElement { + const [status, setStatus] = React.useState(); + + const load = async () => { + setStatus(await ServerApi.NetworkHookStatus()); + }; + + return ( + } + /> + ); +} + +function NetworkHookStatusWidgetInner(p: { + status: NetworkHookStatus; + hiddenIfInstalled: boolean; +}): React.ReactElement { + if (p.status.installed && p.hiddenIfInstalled) return <>; + if (p.status.installed) + return ( + + The network hook has been installed on this system. + + ); + + return ( + + The network hook has not been detected on this system. It must be + installed in order to expose ports from virtual machines through NAT on + the network. +
+
+ In order to install it, please create a file named   + + {p.status.path}   with the following + + content: +
+ + {p.status.content} + +
+ You will need then to restart both + libvirtd + and VirtWeb. +
+ ); +} + +function InlineCode(p: PropsWithChildren): React.ReactElement { + return ( + + {p.children} + + ); +} + +function CodeBlock(p: PropsWithChildren): React.ReactElement { + return ( +
+      {p.children}
+    
+ ); +}