Can define network filters

This commit is contained in:
Pierre HUBERT 2024-01-02 18:56:16 +01:00
parent 2b145ebeff
commit d4ef389852
11 changed files with 349 additions and 43 deletions

View File

@ -1,4 +1,5 @@
use crate::libvirt_client::LibVirtClient; use crate::libvirt_client::LibVirtClient;
use actix_http::StatusCode;
use actix_web::body::BoxBody; use actix_web::body::BoxBody;
use actix_web::{web, HttpResponse}; use actix_web::{web, HttpResponse};
use std::error::Error; use std::error::Error;
@ -32,8 +33,15 @@ impl Display for HttpErr {
} }
impl actix_web::error::ResponseError 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<BoxBody> { fn error_response(&self) -> HttpResponse<BoxBody> {
log::error!("Error while processing request! {}", self); log::error!("Error while processing request! {}", self);
HttpResponse::InternalServerError().body("Failed to execute request!") HttpResponse::InternalServerError().body("Failed to execute request!")
} }
} }

View File

@ -112,10 +112,15 @@ pub async fn update(
id: web::Path<SingleVMUUidReq>, id: web::Path<SingleVMUUidReq>,
req: web::Json<VMInfo>, req: web::Json<VMInfo>,
) -> HttpResult { ) -> HttpResult {
let mut domain = req.0.as_tomain().map_err(|e| { let mut domain = match req.0.as_tomain() {
Ok(d) => d,
Err(e) => {
log::error!("Failed to extract domain info! {e}"); log::error!("Failed to extract domain info! {e}");
return Ok(
HttpResponse::BadRequest().json(format!("Failed to extract domain info! {e}")) HttpResponse::BadRequest().json(format!("Failed to extract domain info! {e}"))
})?; );
}
};
domain.uuid = Some(id.uid); domain.uuid = Some(id.uid);
if let Err(e) = client.update_domain(req.0, domain).await { if let Err(e) = client.update_domain(req.0, domain).await {

View File

@ -63,6 +63,24 @@ pub struct NetIntModelXML {
pub r#type: String, 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<NetIntFilterParameterXML>,
}
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "interface")] #[serde(rename = "interface")]
pub struct DomainNetInterfaceXML { pub struct DomainNetInterfaceXML {
@ -73,6 +91,8 @@ pub struct DomainNetInterfaceXML {
pub source: Option<NetIntSourceXML>, pub source: Option<NetIntSourceXML>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<NetIntModelXML>, pub model: Option<NetIntModelXML>,
#[serde(skip_serializing_if = "Option::is_none")]
pub filterref: Option<NetIntfilterRefXML>,
} }
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]

View File

@ -24,11 +24,24 @@ pub enum VMArchitecture {
X86_64, 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<NWFilterParam>,
}
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
pub struct Network { pub struct Network {
mac: String,
#[serde(flatten)] #[serde(flatten)]
r#type: NetworkType, r#type: NetworkType,
mac: String,
nwfilterref: Option<NWFilterRef>,
} }
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
@ -157,6 +170,67 @@ impl VMInfo {
false => (None, None), 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 // Check disks name for duplicates
for disk in &self.disks { for disk in &self.disks {
if self.disks.iter().filter(|d| d.name == disk.name).count() > 1 { 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 { for disk in &self.disks {
disk.check_config()?; disk.check_config()?;
disk.apply_config(uuid)?; 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 { Ok(DomainXML {
r#type: "kvm".to_string(), r#type: "kvm".to_string(),
name: self.name.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::<Result<Vec<_>, _>>()?, .collect::<Result<Vec<_>, _>>()?,

View File

@ -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<NWFilter[]> {
const list: NWFilter[] = (
await APIClient.exec({
method: "GET",
uri: "/nwfilter/list",
})
).data;
list.sort((a, b) => a.name.localeCompare(b.name));
return list;
}
}

View File

@ -30,16 +30,30 @@ export interface VMDisk {
deleteType?: "keepfile" | "deletefile"; 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 { export interface VMNetUserspaceSLIRPStack {
type: "UserspaceSLIRPStack"; type: "UserspaceSLIRPStack";
mac: string;
} }
export interface VMNetDefinedNetwork { export interface VMNetDefinedNetwork {
type: "DefinedNetwork"; type: "DefinedNetwork";
mac: string;
network: string; network: string;
} }

View File

@ -66,7 +66,7 @@ export function EditVMRoute(): React.ReactElement {
navigate(v.ViewURL); navigate(v.ViewURL);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
alert("Failed to update VM info!"); alert(`Failed to update VM info!\n${e}`);
} }
}; };

View File

@ -5,7 +5,7 @@ import { LenConstraint } from "../../api/ServerApi";
* Couple / Member property edition * Couple / Member property edition
*/ */
export function TextInput(p: { export function TextInput(p: {
label: string; label?: string;
editable: boolean; editable: boolean;
value?: string; value?: string;
onValueChange?: (newVal: string | undefined) => void; onValueChange?: (newVal: string | undefined) => void;
@ -15,6 +15,7 @@ export function TextInput(p: {
minRows?: number; minRows?: number;
maxRows?: number; maxRows?: number;
type?: React.HTMLInputTypeAttribute; type?: React.HTMLInputTypeAttribute;
style?: React.CSSProperties;
}): React.ReactElement { }): React.ReactElement {
if (!p.editable && (p.value ?? "") === "") return <></>; if (!p.editable && (p.value ?? "") === "") return <></>;
@ -48,7 +49,7 @@ export function TextInput(p: {
type: p.type, type: p.type,
}} }}
variant={"standard"} variant={"standard"}
style={{ width: "100%", marginBottom: "15px" }} style={p.style ?? { width: "100%", marginBottom: "15px" }}
multiline={p.multiline} multiline={p.multiline}
minRows={p.minRows} minRows={p.minRows}
maxRows={p.maxRows} maxRows={p.maxRows}

View File

@ -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 && (
<TableContainer component={Paper}>
<Table size="small" aria-label="nwfilter parameters">
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Value</TableCell>
{p.editable && <TableCell></TableCell>}
</TableRow>
</TableHead>
<TableBody>
{p.filterref.parameters.map((row, index) => (
<TableRow
key={index}
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
>
<TableCell
component="th"
scope="row"
style={{ padding: "0px 5px" }}
>
<TextInput
editable={p.editable}
value={row.name}
onValueChange={(v) => {
row.name = v ?? "";
p.onChange?.();
}}
/>
</TableCell>
<TableCell scope="row" style={{ padding: "0px 5px" }}>
<TextInput
editable={p.editable}
value={row.value}
onValueChange={(v) => {
row.value = v ?? "";
p.onChange?.();
}}
/>
</TableCell>
{p.editable && (
<TableCell style={{ padding: "0px" }}>
<IconButton
onClick={() => {
p.filterref.parameters.splice(index, 1);
p.onChange?.();
}}
>
<Tooltip title="Remove parameter">
<DeleteIcon />
</Tooltip>
</IconButton>
</TableCell>
)}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
{p.editable && (
<Button onClick={addParameter}>Add a filter ref parameter</Button>
)}
</>
);
}

View File

@ -10,19 +10,22 @@ import {
ListItemText, ListItemText,
Tooltip, Tooltip,
} from "@mui/material"; } 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 { VMInfo, VMNetInterface } from "../../api/VMApi";
import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider"; import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
import { SelectInput } from "./SelectInput";
import { NetworkInfo } from "../../api/NetworksApi";
import { randomMacAddress } from "../../utils/RandUtils"; import { randomMacAddress } from "../../utils/RandUtils";
import { ServerApi } from "../../api/ServerApi";
import { MACInput } from "./MACInput"; import { MACInput } from "./MACInput";
import { SelectInput } from "./SelectInput";
import { VMNetworkFilterParameters } from "./VMNetworkFilterParameters";
export function VMNetworksList(p: { export function VMNetworksList(p: {
vm: VMInfo; vm: VMInfo;
onChange?: () => void; onChange?: () => void;
editable: boolean; editable: boolean;
networksList: NetworkInfo[]; networksList: NetworkInfo[];
networkFiltersList: NWFilter[];
}): React.ReactElement { }): React.ReactElement {
const addNew = () => { const addNew = () => {
p.vm.networks.push({ p.vm.networks.push({
@ -60,6 +63,7 @@ function NetworkInfoWidget(p: {
onChange?: () => void; onChange?: () => void;
removeFromList: () => void; removeFromList: () => void;
networksList: NetworkInfo[]; networksList: NetworkInfo[];
networkFiltersList: NWFilter[];
}): React.ReactElement { }): React.ReactElement {
const confirm = useConfirm(); const confirm = useConfirm();
const deleteNetwork = async () => { const deleteNetwork = async () => {
@ -160,6 +164,42 @@ function NetworkInfoWidget(p: {
}} }}
/> />
)} )}
{/* Network Filter */}
<SelectInput
editable={p.editable}
label="Network filter"
value={p.network.nwfilterref?.name}
onValueChange={(v) => {
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 && (
<div style={{ margin: "10px" }}>
<VMNetworkFilterParameters
filterref={p.network.nwfilterref}
{...p}
/>
</div>
)}
</div> </div>
</> </>
); );

View File

@ -15,6 +15,7 @@ import { VMScreenshot } from "./VMScreenshot";
import { ResAutostartInput } from "../forms/ResAutostartInput"; import { ResAutostartInput } from "../forms/ResAutostartInput";
import { VMNetworksList } from "../forms/VMNetworksList"; import { VMNetworksList } from "../forms/VMNetworksList";
import { NetworkApi, NetworkInfo } from "../../api/NetworksApi"; import { NetworkApi, NetworkInfo } from "../../api/NetworksApi";
import { NWFilterApi, NWFilter } from "../../api/NWFilterApi";
interface DetailsProps { interface DetailsProps {
vm: VMInfo; vm: VMInfo;
@ -29,11 +30,15 @@ export function VMDetails(p: DetailsProps): React.ReactElement {
number[] | any number[] | any
>(); >();
const [networksList, setNetworksList] = React.useState<NetworkInfo[] | any>(); const [networksList, setNetworksList] = React.useState<NetworkInfo[] | any>();
const [networkFiltersList, setNetworkFiltersList] = React.useState<
NWFilter[] | any
>();
const load = async () => { const load = async () => {
setIsoList(await IsoFilesApi.GetList()); setIsoList(await IsoFilesApi.GetList());
setVCPUCombinations(await ServerApi.NumberVCPUs()); setVCPUCombinations(await ServerApi.NumberVCPUs());
setNetworksList(await NetworkApi.GetList()); setNetworksList(await NetworkApi.GetList());
setNetworkFiltersList(await NWFilterApi.GetList());
}; };
return ( return (
@ -46,6 +51,7 @@ export function VMDetails(p: DetailsProps): React.ReactElement {
isoList={isoList} isoList={isoList}
vcpuCombinations={vcpuCombinations} vcpuCombinations={vcpuCombinations}
networksList={networksList} networksList={networksList}
networkFiltersList={networkFiltersList}
{...p} {...p}
/> />
)} )}
@ -58,6 +64,7 @@ function VMDetailsInner(
isoList: IsoFile[]; isoList: IsoFile[];
vcpuCombinations: number[]; vcpuCombinations: number[];
networksList: NetworkInfo[]; networksList: NetworkInfo[];
networkFiltersList: NWFilter[];
} }
): React.ReactElement { ): React.ReactElement {
return ( return (