Show guidelines on UI on how to setup network hook
This commit is contained in:
parent
d6c8964380
commit
4d0b4929c5
@ -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>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user