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
 | 
					/// Environment variable that is set to run VirtWeb in NAT configuration mode
 | 
				
			||||||
pub const NAT_MODE_ENV_VAR_NAME: &str = "NAT_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::controllers::{HttpResult, LibVirtReq};
 | 
				
			||||||
use crate::extractors::local_auth_extractor::LocalAuthEnabled;
 | 
					use crate::extractors::local_auth_extractor::LocalAuthEnabled;
 | 
				
			||||||
use crate::libvirt_rest_structures::hypervisor::HypervisorInfo;
 | 
					use crate::libvirt_rest_structures::hypervisor::HypervisorInfo;
 | 
				
			||||||
 | 
					use crate::nat::nat_hook;
 | 
				
			||||||
use crate::utils::net_utils;
 | 
					use crate::utils::net_utils;
 | 
				
			||||||
use actix_web::{HttpResponse, Responder};
 | 
					use actix_web::{HttpResponse, Responder};
 | 
				
			||||||
use sysinfo::{System, SystemExt};
 | 
					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 {
 | 
					pub async fn number_vcpus() -> HttpResult {
 | 
				
			||||||
    let mut system = System::new();
 | 
					    let mut system = System::new();
 | 
				
			||||||
    system.refresh_cpu();
 | 
					    system.refresh_cpu();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -118,6 +118,10 @@ async fn main() -> std::io::Result<()> {
 | 
				
			|||||||
                "/api/server/info",
 | 
					                "/api/server/info",
 | 
				
			||||||
                web::get().to(server_controller::server_info),
 | 
					                web::get().to(server_controller::server_info),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					            .route(
 | 
				
			||||||
 | 
					                "/api/server/network_hook_status",
 | 
				
			||||||
 | 
					                web::get().to(server_controller::network_hook_status),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
            .route(
 | 
					            .route(
 | 
				
			||||||
                "/api/server/number_vcpus",
 | 
					                "/api/server/number_vcpus",
 | 
				
			||||||
                web::get().to(server_controller::number_vcpus),
 | 
					                web::get().to(server_controller::number_vcpus),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,4 @@
 | 
				
			|||||||
pub mod nat_conf_mode;
 | 
					pub mod nat_conf_mode;
 | 
				
			||||||
pub mod nat_definition;
 | 
					pub mod nat_definition;
 | 
				
			||||||
 | 
					pub mod nat_hook;
 | 
				
			||||||
pub mod nat_lib;
 | 
					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;
 | 
					  fifteen: number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface NetworkHookStatus {
 | 
				
			||||||
 | 
					  installed: boolean;
 | 
				
			||||||
 | 
					  content: string;
 | 
				
			||||||
 | 
					  path: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class ServerApi {
 | 
					export class ServerApi {
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Get server configuration
 | 
					   * Get server configuration
 | 
				
			||||||
@@ -181,6 +187,18 @@ export class ServerApi {
 | 
				
			|||||||
    ).data;
 | 
					    ).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
 | 
					   * Get host supported vCPUs configurations
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,6 +19,7 @@ import { RouterLink } from "../widgets/RouterLink";
 | 
				
			|||||||
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
 | 
					import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
 | 
				
			||||||
import { NetworkStatusWidget } from "../widgets/net/NetworkStatusWidget";
 | 
					import { NetworkStatusWidget } from "../widgets/net/NetworkStatusWidget";
 | 
				
			||||||
import { useNavigate } from "react-router-dom";
 | 
					import { useNavigate } from "react-router-dom";
 | 
				
			||||||
 | 
					import { NetworkHookStatusWidget } from "../widgets/net/NetworkHookStatusWidget";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function NetworksListRoute(): React.ReactElement {
 | 
					export function NetworksListRoute(): React.ReactElement {
 | 
				
			||||||
  const [list, setList] = React.useState<NetworkInfo[] | undefined>();
 | 
					  const [list, setList] = React.useState<NetworkInfo[] | undefined>();
 | 
				
			||||||
@@ -54,6 +55,8 @@ function NetworksListRouteInner(p: {
 | 
				
			|||||||
        </RouterLink>
 | 
					        </RouterLink>
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
 | 
					      <NetworkHookStatusWidget hiddenIfInstalled />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <TableContainer component={Paper}>
 | 
					      <TableContainer component={Paper}>
 | 
				
			||||||
        <Table>
 | 
					        <Table>
 | 
				
			||||||
          <TableHead>
 | 
					          <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