diff --git a/virtweb_backend/src/controllers/mod.rs b/virtweb_backend/src/controllers/mod.rs index 08cf587..402d2d4 100644 --- a/virtweb_backend/src/controllers/mod.rs +++ b/virtweb_backend/src/controllers/mod.rs @@ -1,4 +1,5 @@ use crate::libvirt_client::LibVirtClient; +use actix_http::StatusCode; use actix_web::body::BoxBody; use actix_web::{web, HttpResponse}; use std::error::Error; @@ -32,8 +33,15 @@ impl Display for HttpErr { } impl actix_web::error::ResponseError for HttpErr { + fn status_code(&self) -> StatusCode { + match self { + HttpErr::Err(_) => StatusCode::INTERNAL_SERVER_ERROR, + HttpErr::HTTPResponse(r) => r.status(), + } + } fn error_response(&self) -> HttpResponse { log::error!("Error while processing request! {}", self); + HttpResponse::InternalServerError().body("Failed to execute request!") } } diff --git a/virtweb_backend/src/controllers/vm_controller.rs b/virtweb_backend/src/controllers/vm_controller.rs index d5c9d70..e954353 100644 --- a/virtweb_backend/src/controllers/vm_controller.rs +++ b/virtweb_backend/src/controllers/vm_controller.rs @@ -112,10 +112,15 @@ pub async fn update( id: web::Path, req: web::Json, ) -> HttpResult { - let mut domain = req.0.as_tomain().map_err(|e| { - log::error!("Failed to extract domain info! {e}"); - HttpResponse::BadRequest().json(format!("Failed to extract domain info! {e}")) - })?; + let mut domain = match req.0.as_tomain() { + Ok(d) => d, + Err(e) => { + log::error!("Failed to extract domain info! {e}"); + return Ok( + HttpResponse::BadRequest().json(format!("Failed to extract domain info! {e}")) + ); + } + }; domain.uuid = Some(id.uid); if let Err(e) = client.update_domain(req.0, domain).await { diff --git a/virtweb_backend/src/libvirt_lib_structures/domain.rs b/virtweb_backend/src/libvirt_lib_structures/domain.rs index 7cef0cb..df80384 100644 --- a/virtweb_backend/src/libvirt_lib_structures/domain.rs +++ b/virtweb_backend/src/libvirt_lib_structures/domain.rs @@ -63,6 +63,24 @@ pub struct NetIntModelXML { pub r#type: String, } +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "filterref")] +pub struct NetIntFilterParameterXML { + #[serde(rename = "@name")] + pub name: String, + #[serde(rename = "@value")] + pub value: String, +} + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "filterref")] +pub struct NetIntfilterRefXML { + #[serde(rename = "@filter")] + pub filter: String, + #[serde(rename = "parameter", default)] + pub parameters: Vec, +} + #[derive(serde::Serialize, serde::Deserialize)] #[serde(rename = "interface")] pub struct DomainNetInterfaceXML { @@ -73,6 +91,8 @@ pub struct DomainNetInterfaceXML { pub source: Option, #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub filterref: Option, } #[derive(serde::Serialize, serde::Deserialize)] diff --git a/virtweb_backend/src/libvirt_rest_structures/vm.rs b/virtweb_backend/src/libvirt_rest_structures/vm.rs index cad1bc9..7e93e2c 100644 --- a/virtweb_backend/src/libvirt_rest_structures/vm.rs +++ b/virtweb_backend/src/libvirt_rest_structures/vm.rs @@ -24,11 +24,24 @@ pub enum VMArchitecture { X86_64, } +#[derive(serde::Serialize, serde::Deserialize)] +pub struct NWFilterParam { + name: String, + value: String, +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct NWFilterRef { + name: String, + parameters: Vec, +} + #[derive(serde::Serialize, serde::Deserialize)] pub struct Network { - mac: String, #[serde(flatten)] r#type: NetworkType, + mac: String, + nwfilterref: Option, } #[derive(serde::Serialize, serde::Deserialize)] @@ -157,6 +170,67 @@ impl VMInfo { false => (None, None), }; + // Process network card + let mut networks = vec![]; + for n in &self.networks { + let mac = NetMacAddress { + address: n.mac.to_string(), + }; + + let model = Some(NetIntModelXML { + r#type: "virtio".to_string(), + }); + + let filterref = if let Some(n) = &n.nwfilterref { + if !regex!("^[a-zA-Z0-9\\_\\-]+$").is_match(&n.name) { + log::error!("Filter ref name {} is invalid", n.name); + return Err(StructureExtraction("Network filter ref name is invalid!").into()); + } + + for p in &n.parameters { + if !regex!("^[a-zA-Z0-9_-]+$").is_match(&p.name) { + return Err(StructureExtraction( + "Network filter ref parameter name is invalid!", + ) + .into()); + } + } + + Some(NetIntfilterRefXML { + filter: n.name.to_string(), + parameters: n + .parameters + .iter() + .map(|f| NetIntFilterParameterXML { + name: f.name.to_string(), + value: f.value.to_string(), + }) + .collect(), + }) + } else { + None + }; + + networks.push(match &n.r#type { + NetworkType::UserspaceSLIRPStack => DomainNetInterfaceXML { + mac, + r#type: "user".to_string(), + source: None, + model, + filterref, + }, + NetworkType::DefinedNetwork { network } => DomainNetInterfaceXML { + mac, + r#type: "network".to_string(), + source: Some(NetIntSourceXML { + network: network.to_string(), + }), + model, + filterref, + }, + }) + } + // Check disks name for duplicates for disk in &self.disks { if self.disks.iter().filter(|d| d.name == disk.name).count() > 1 { @@ -164,7 +238,8 @@ impl VMInfo { } } - // Apply disks configuration + // Apply disks configuration. Starting from now, the function should ideally never fail due to + // bad user input for disk in &self.disks { disk.check_config()?; disk.apply_config(uuid)?; @@ -199,34 +274,6 @@ impl VMInfo { }) } - let mut networks = vec![]; - for n in &self.networks { - networks.push(match &n.r#type { - NetworkType::UserspaceSLIRPStack => DomainNetInterfaceXML { - mac: NetMacAddress { - address: n.mac.to_string(), - }, - r#type: "user".to_string(), - source: None, - model: Some(NetIntModelXML { - r#type: "virtio".to_string(), - }), - }, - NetworkType::DefinedNetwork { network } => DomainNetInterfaceXML { - mac: NetMacAddress { - address: n.mac.to_string(), - }, - r#type: "network".to_string(), - source: Some(NetIntSourceXML { - network: network.to_string(), - }), - model: Some(NetIntModelXML { - r#type: "virtio".to_string(), - }), - }, - }) - } - Ok(DomainXML { r#type: "kvm".to_string(), name: self.name.to_string(), @@ -376,6 +423,17 @@ impl VMInfo { ))); } }, + nwfilterref: d.filterref.as_ref().map(|f| NWFilterRef { + name: f.filter.to_string(), + parameters: f + .parameters + .iter() + .map(|p| NWFilterParam { + name: p.name.to_string(), + value: p.value.to_string(), + }) + .collect(), + }), }) }) .collect::, _>>()?, diff --git a/virtweb_frontend/src/api/NWFilterApi.ts b/virtweb_frontend/src/api/NWFilterApi.ts new file mode 100644 index 0000000..ae6fc4a --- /dev/null +++ b/virtweb_frontend/src/api/NWFilterApi.ts @@ -0,0 +1,56 @@ +import { APIClient } from "./ApiClient"; + +export interface NWFilterChain { + protocol: string; + suffix?: string; +} + +export interface NWFSAll { + type: "all"; +} + +export interface NWFSMac { + type: "mac"; + src_mac_addr?: string; + src_mac_mask?: string; + dst_mac_addr?: string; + dst_mac_mask?: string; + comment?: string; +} + +// TODO : complete +export type NWFSelector = NWFSAll | NWFSMac; + +export interface NWFilterRule { + action: "drop" | "reject" | "accept" | "return" | "continue"; + direction: "in" | "out" | "inout"; + priority?: number; + selectors: NWFSelector[]; +} + +export interface NWFilter { + name: string; + chain?: NWFilterChain; + priority?: number; + uuid?: string; + join_filters: string[]; + rules: NWFilterRule[]; +} + +export class NWFilterApi { + /** + * Get the entire list of networks + */ + static async GetList(): Promise { + const list: NWFilter[] = ( + await APIClient.exec({ + method: "GET", + uri: "/nwfilter/list", + }) + ).data; + + list.sort((a, b) => a.name.localeCompare(b.name)); + + return list; + } +} diff --git a/virtweb_frontend/src/api/VMApi.ts b/virtweb_frontend/src/api/VMApi.ts index 48655af..12564ef 100644 --- a/virtweb_frontend/src/api/VMApi.ts +++ b/virtweb_frontend/src/api/VMApi.ts @@ -30,16 +30,30 @@ export interface VMDisk { deleteType?: "keepfile" | "deletefile"; } -export type VMNetInterface = VMNetUserspaceSLIRPStack | VMNetDefinedNetwork; +export interface VMNetInterfaceFilterParams { + name: string; + value: string; +} + +export interface VMNetInterfaceFilter { + name: string; + parameters: VMNetInterfaceFilterParams[]; +} + +export type VMNetInterface = (VMNetUserspaceSLIRPStack | VMNetDefinedNetwork) & + VMNetInterfaceBase; + +export interface VMNetInterfaceBase { + mac: string; + nwfilterref?: VMNetInterfaceFilter; +} export interface VMNetUserspaceSLIRPStack { type: "UserspaceSLIRPStack"; - mac: string; } export interface VMNetDefinedNetwork { type: "DefinedNetwork"; - mac: string; network: string; } diff --git a/virtweb_frontend/src/routes/EditVMRoute.tsx b/virtweb_frontend/src/routes/EditVMRoute.tsx index 36b1b7b..93f3014 100644 --- a/virtweb_frontend/src/routes/EditVMRoute.tsx +++ b/virtweb_frontend/src/routes/EditVMRoute.tsx @@ -66,7 +66,7 @@ export function EditVMRoute(): React.ReactElement { navigate(v.ViewURL); } catch (e) { console.error(e); - alert("Failed to update VM info!"); + alert(`Failed to update VM info!\n${e}`); } }; diff --git a/virtweb_frontend/src/widgets/forms/TextInput.tsx b/virtweb_frontend/src/widgets/forms/TextInput.tsx index a83680a..cb6e3b0 100644 --- a/virtweb_frontend/src/widgets/forms/TextInput.tsx +++ b/virtweb_frontend/src/widgets/forms/TextInput.tsx @@ -5,7 +5,7 @@ import { LenConstraint } from "../../api/ServerApi"; * Couple / Member property edition */ export function TextInput(p: { - label: string; + label?: string; editable: boolean; value?: string; onValueChange?: (newVal: string | undefined) => void; @@ -15,6 +15,7 @@ export function TextInput(p: { minRows?: number; maxRows?: number; type?: React.HTMLInputTypeAttribute; + style?: React.CSSProperties; }): React.ReactElement { if (!p.editable && (p.value ?? "") === "") return <>; @@ -48,7 +49,7 @@ export function TextInput(p: { type: p.type, }} variant={"standard"} - style={{ width: "100%", marginBottom: "15px" }} + style={p.style ?? { width: "100%", marginBottom: "15px" }} multiline={p.multiline} minRows={p.minRows} maxRows={p.maxRows} diff --git a/virtweb_frontend/src/widgets/forms/VMNetworkFilterParameters.tsx b/virtweb_frontend/src/widgets/forms/VMNetworkFilterParameters.tsx new file mode 100644 index 0000000..26177e3 --- /dev/null +++ b/virtweb_frontend/src/widgets/forms/VMNetworkFilterParameters.tsx @@ -0,0 +1,97 @@ +import DeleteIcon from "@mui/icons-material/Delete"; +import { + Button, + IconButton, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Tooltip, +} from "@mui/material"; +import { VMNetInterfaceFilter } from "../../api/VMApi"; +import { TextInput } from "./TextInput"; + +export function VMNetworkFilterParameters(p: { + editable: boolean; + filterref: VMNetInterfaceFilter; + onChange?: () => void; +}): React.ReactElement { + if (!p.editable && p.filterref.parameters.length === 0) return <>; + + const addParameter = () => { + p.filterref.parameters.push({ name: "", value: "" }); + p.onChange?.(); + }; + + return ( + <> + {p.filterref.parameters.length > 0 && ( + + + + + Name + Value + {p.editable && } + + + + {p.filterref.parameters.map((row, index) => ( + + + { + row.name = v ?? ""; + p.onChange?.(); + }} + /> + + + { + row.value = v ?? ""; + p.onChange?.(); + }} + /> + + {p.editable && ( + + { + p.filterref.parameters.splice(index, 1); + p.onChange?.(); + }} + > + + + + + + )} + + ))} + +
+
+ )} + + {p.editable && ( + + )} + + ); +} diff --git a/virtweb_frontend/src/widgets/forms/VMNetworksList.tsx b/virtweb_frontend/src/widgets/forms/VMNetworksList.tsx index 5774e31..d3bfbe6 100644 --- a/virtweb_frontend/src/widgets/forms/VMNetworksList.tsx +++ b/virtweb_frontend/src/widgets/forms/VMNetworksList.tsx @@ -10,19 +10,22 @@ import { ListItemText, Tooltip, } from "@mui/material"; +import { NWFilter } from "../../api/NWFilterApi"; +import { NetworkInfo } from "../../api/NetworksApi"; +import { ServerApi } from "../../api/ServerApi"; import { VMInfo, VMNetInterface } from "../../api/VMApi"; import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider"; -import { SelectInput } from "./SelectInput"; -import { NetworkInfo } from "../../api/NetworksApi"; import { randomMacAddress } from "../../utils/RandUtils"; -import { ServerApi } from "../../api/ServerApi"; import { MACInput } from "./MACInput"; +import { SelectInput } from "./SelectInput"; +import { VMNetworkFilterParameters } from "./VMNetworkFilterParameters"; export function VMNetworksList(p: { vm: VMInfo; onChange?: () => void; editable: boolean; networksList: NetworkInfo[]; + networkFiltersList: NWFilter[]; }): React.ReactElement { const addNew = () => { p.vm.networks.push({ @@ -60,6 +63,7 @@ function NetworkInfoWidget(p: { onChange?: () => void; removeFromList: () => void; networksList: NetworkInfo[]; + networkFiltersList: NWFilter[]; }): React.ReactElement { const confirm = useConfirm(); const deleteNetwork = async () => { @@ -160,6 +164,42 @@ function NetworkInfoWidget(p: { }} /> )} + + {/* Network Filter */} + { + if (v && !p.network.nwfilterref) { + p.network.nwfilterref = { name: v, parameters: [] }; + } else if (v) { + p.network.nwfilterref!.name = v; + } else { + p.network.nwfilterref = undefined; + } + p.onChange?.(); + }} + options={[ + { label: "No network filer", value: undefined }, + ...p.networkFiltersList.map((v) => { + return { + value: v.name, + label: `${v.name} (${v.chain?.protocol ?? "unspecified"})`, + description: `${v.rules.length} rules - ${v.join_filters.length} joint filters`, + }; + }), + ]} + /> + + {p.network.nwfilterref && ( +
+ +
+ )} ); diff --git a/virtweb_frontend/src/widgets/vms/VMDetails.tsx b/virtweb_frontend/src/widgets/vms/VMDetails.tsx index 3125359..3904b90 100644 --- a/virtweb_frontend/src/widgets/vms/VMDetails.tsx +++ b/virtweb_frontend/src/widgets/vms/VMDetails.tsx @@ -15,6 +15,7 @@ import { VMScreenshot } from "./VMScreenshot"; import { ResAutostartInput } from "../forms/ResAutostartInput"; import { VMNetworksList } from "../forms/VMNetworksList"; import { NetworkApi, NetworkInfo } from "../../api/NetworksApi"; +import { NWFilterApi, NWFilter } from "../../api/NWFilterApi"; interface DetailsProps { vm: VMInfo; @@ -29,11 +30,15 @@ export function VMDetails(p: DetailsProps): React.ReactElement { number[] | any >(); const [networksList, setNetworksList] = React.useState(); + const [networkFiltersList, setNetworkFiltersList] = React.useState< + NWFilter[] | any + >(); const load = async () => { setIsoList(await IsoFilesApi.GetList()); setVCPUCombinations(await ServerApi.NumberVCPUs()); setNetworksList(await NetworkApi.GetList()); + setNetworkFiltersList(await NWFilterApi.GetList()); }; return ( @@ -46,6 +51,7 @@ export function VMDetails(p: DetailsProps): React.ReactElement { isoList={isoList} vcpuCombinations={vcpuCombinations} networksList={networksList} + networkFiltersList={networkFiltersList} {...p} /> )} @@ -58,6 +64,7 @@ function VMDetailsInner( isoList: IsoFile[]; vcpuCombinations: number[]; networksList: NetworkInfo[]; + networkFiltersList: NWFilter[]; } ): React.ReactElement { return (