Show guidelines on UI on how to setup network hook
This commit is contained in:
		| @@ -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"; | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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), | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| pub mod nat_conf_mode; | ||||
| pub mod nat_definition; | ||||
| pub mod nat_hook; | ||||
| pub mod nat_lib; | ||||
|   | ||||
							
								
								
									
										29
									
								
								virtweb_backend/src/nat/nat_hook.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								virtweb_backend/src/nat/nat_hook.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -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<bool> { | ||||
|     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<String> { | ||||
|     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 | ||||
|     )) | ||||
| } | ||||
| @@ -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<NetworkHookStatus> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         method: "GET", | ||||
|         uri: "/server/network_hook_status", | ||||
|       }) | ||||
|     ).data; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get host supported vCPUs configurations | ||||
|    */ | ||||
|   | ||||
| @@ -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<NetworkInfo[] | undefined>(); | ||||
| @@ -54,6 +55,8 @@ function NetworksListRouteInner(p: { | ||||
|         </RouterLink> | ||||
|       } | ||||
|     > | ||||
|       <NetworkHookStatusWidget hiddenIfInstalled /> | ||||
|  | ||||
|       <TableContainer component={Paper}> | ||||
|         <Table> | ||||
|           <TableHead> | ||||
|   | ||||
							
								
								
									
										30
									
								
								virtweb_frontend/src/widgets/CopyToClipboard.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								virtweb_frontend/src/widgets/CopyToClipboard.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -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 ( | ||||
|     <ButtonBase | ||||
|       onClick={copy} | ||||
|       style={{ | ||||
|         display: "inline-block", | ||||
|         alignItems: "unset", | ||||
|         textAlign: "unset", | ||||
|         position: "relative", | ||||
|         padding: "0px", | ||||
|       }} | ||||
|       disableRipple | ||||
|     > | ||||
|       {p.children} | ||||
|     </ButtonBase> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										102
									
								
								virtweb_frontend/src/widgets/net/NetworkHookStatusWidget.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								virtweb_frontend/src/widgets/net/NetworkHookStatusWidget.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -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<NetworkHookStatus | undefined>(); | ||||
|  | ||||
|   const load = async () => { | ||||
|     setStatus(await ServerApi.NetworkHookStatus()); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <AsyncWidget | ||||
|       loadKey={1} | ||||
|       errMsg="Failed to get network status!" | ||||
|       ready={!!status} | ||||
|       load={load} | ||||
|       build={() => <NetworkHookStatusWidgetInner {...p} status={status!} />} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NetworkHookStatusWidgetInner(p: { | ||||
|   status: NetworkHookStatus; | ||||
|   hiddenIfInstalled: boolean; | ||||
| }): React.ReactElement { | ||||
|   if (p.status.installed && p.hiddenIfInstalled) return <></>; | ||||
|   if (p.status.installed) | ||||
|     return ( | ||||
|       <Alert | ||||
|         variant="outlined" | ||||
|         severity="success" | ||||
|         style={{ margin: "20px 0px" }} | ||||
|       > | ||||
|         The network hook has been installed on this system. | ||||
|       </Alert> | ||||
|     ); | ||||
|  | ||||
|   return ( | ||||
|     <Alert variant="outlined" severity="warning" style={{ margin: "20px 0px" }}> | ||||
|       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. | ||||
|       <br /> | ||||
|       <br /> | ||||
|       In order to install it, please create a file named   | ||||
|       <CopyToClipboard content={p.status.path}> | ||||
|         <InlineCode>{p.status.path}</InlineCode>   with the following | ||||
|       </CopyToClipboard> | ||||
|       content: | ||||
|       <br /> | ||||
|       <CopyToClipboard content={p.status.content}> | ||||
|         <CodeBlock>{p.status.content}</CodeBlock> | ||||
|       </CopyToClipboard> | ||||
|       <br /> | ||||
|       You will need then to restart both <InlineCode> | ||||
|         libvirtd | ||||
|       </InlineCode> and <InlineCode>VirtWeb</InlineCode>. | ||||
|     </Alert> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function InlineCode(p: PropsWithChildren): React.ReactElement { | ||||
|   return ( | ||||
|     <code | ||||
|       style={{ | ||||
|         display: "inline-block", | ||||
|         backgroundColor: "black", | ||||
|         color: "white", | ||||
|         wordBreak: "break-all", | ||||
|         wordWrap: "break-word", | ||||
|         whiteSpace: "pre-wrap", | ||||
|         padding: "0px 7px", | ||||
|         borderRadius: "5px", | ||||
|       }} | ||||
|     > | ||||
|       {p.children} | ||||
|     </code> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function CodeBlock(p: PropsWithChildren): React.ReactElement { | ||||
|   return ( | ||||
|     <pre | ||||
|       style={{ | ||||
|         backgroundColor: "black", | ||||
|         color: "white", | ||||
|         wordBreak: "break-all", | ||||
|         wordWrap: "break-word", | ||||
|         whiteSpace: "pre-wrap", | ||||
|         padding: "10px", | ||||
|         borderRadius: "5px", | ||||
|       }} | ||||
|     > | ||||
|       {p.children} | ||||
|     </pre> | ||||
|   ); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user