Can define network filters
This commit is contained in:
parent
2b145ebeff
commit
d4ef389852
@ -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<BoxBody> {
|
||||
log::error!("Error while processing request! {}", self);
|
||||
|
||||
HttpResponse::InternalServerError().body("Failed to execute request!")
|
||||
}
|
||||
}
|
||||
|
@ -112,10 +112,15 @@ pub async fn update(
|
||||
id: web::Path<SingleVMUUidReq>,
|
||||
req: web::Json<VMInfo>,
|
||||
) -> 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 {
|
||||
|
@ -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<NetIntFilterParameterXML>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "interface")]
|
||||
pub struct DomainNetInterfaceXML {
|
||||
@ -73,6 +91,8 @@ pub struct DomainNetInterfaceXML {
|
||||
pub source: Option<NetIntSourceXML>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub model: Option<NetIntModelXML>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub filterref: Option<NetIntfilterRefXML>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
|
@ -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<NWFilterParam>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct Network {
|
||||
mac: String,
|
||||
#[serde(flatten)]
|
||||
r#type: NetworkType,
|
||||
mac: String,
|
||||
nwfilterref: Option<NWFilterRef>,
|
||||
}
|
||||
|
||||
#[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::<Result<Vec<_>, _>>()?,
|
||||
|
56
virtweb_frontend/src/api/NWFilterApi.ts
Normal file
56
virtweb_frontend/src/api/NWFilterApi.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -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 */}
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
|
@ -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<NetworkInfo[] | any>();
|
||||
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 (
|
||||
|
Loading…
Reference in New Issue
Block a user