Show guidelines on UI on how to setup network hook

This commit is contained in:
Pierre HUBERT 2024-01-11 19:02:47 +01:00
parent d6c8964380
commit 4d0b4929c5
9 changed files with 206 additions and 0 deletions

View File

@ -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";

View File

@ -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();

View File

@ -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),

View File

@ -1,3 +1,4 @@
pub mod nat_conf_mode;
pub mod nat_definition;
pub mod nat_hook;
pub mod nat_lib;

View 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
))
}

View File

@ -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
*/

View File

@ -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>

View 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>
);
}

View 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 &nbsp;
<CopyToClipboard content={p.status.path}>
<InlineCode>{p.status.path}</InlineCode> &nbsp; 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>
);
}