diff --git a/virtweb_backend/src/controllers/server_controller.rs b/virtweb_backend/src/controllers/server_controller.rs index 9849fbb..46db904 100644 --- a/virtweb_backend/src/controllers/server_controller.rs +++ b/virtweb_backend/src/controllers/server_controller.rs @@ -35,6 +35,7 @@ struct ServerConstraints { disk_size: LenConstraints, net_name_size: LenConstraints, net_title_size: LenConstraints, + dhcp_reservation_host_name: LenConstraints, } pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder { @@ -66,6 +67,8 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder { net_name_size: LenConstraints { min: 2, max: 50 }, net_title_size: LenConstraints { min: 0, max: 50 }, + + dhcp_reservation_host_name: LenConstraints { min: 2, max: 250 }, }, }) } diff --git a/virtweb_backend/src/libvirt_lib_structures.rs b/virtweb_backend/src/libvirt_lib_structures.rs index f348603..d17d136 100644 --- a/virtweb_backend/src/libvirt_lib_structures.rs +++ b/virtweb_backend/src/libvirt_lib_structures.rs @@ -432,10 +432,49 @@ pub struct NetworkIPXML { pub dhcp: Option, } +impl NetworkIPXML { + pub fn into_xml(mut self) -> anyhow::Result { + let mut hosts_xml = vec![]; + + if let Some(dhcp) = &mut self.dhcp { + for host in &dhcp.hosts { + let mut host_xml = serde_xml_rs::to_string(&host)?; + + // In case of IPv6, mac address should not be specified + host_xml = host_xml.replace("mac=\"\"", ""); + + // strip xml tag + let start_offset = host_xml.find("", &format!("{hosts_xml}")); + Ok(res) + } +} + #[derive(serde::Serialize, serde::Deserialize, Debug)] #[serde(rename = "dhcp")] pub struct NetworkDHCPXML { pub range: NetworkDHCPRangeXML, + #[serde(default, rename = "host", skip_serializing_if = "Vec::is_empty")] + pub hosts: Vec, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename = "host")] +pub struct NetworkDHCPHostXML { + #[serde(rename(serialize = "@mac"), default)] + pub mac: String, + #[serde(rename(serialize = "@name"))] + pub name: String, + #[serde(rename(serialize = "@ip"))] + pub ip: IpAddr, } #[derive(serde::Serialize, serde::Deserialize, Debug)] @@ -474,7 +513,8 @@ impl NetworkXML { let mut ips_xml = Vec::with_capacity(self.ips.len()); for ip in self.ips { log::debug!("Serialize {ip:?}"); - let ip_xml = serde_xml_rs::to_string(&ip)?; + let ip_xml = ip.into_xml()?; + // strip xml tag let start_offset = ip_xml.find(", +} + #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] pub struct IPV4Config { bridge_address: Ipv4Addr, prefix: u32, - dhcp_range: Option<[Ipv4Addr; 2]>, + dhcp: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] +pub struct DHCPv6HostReservation { + name: String, + ip: Ipv6Addr, +} + +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] +pub struct IPv6DHCPConfig { + start: Ipv6Addr, + end: Ipv6Addr, + hosts: Vec, } #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] pub struct IPV6Config { bridge_address: Ipv6Addr, prefix: u32, - dhcp_range: Option<[Ipv6Addr; 2]>, + dhcp: Option, } /// Network configuration @@ -510,11 +537,20 @@ impl NetworkInfo { .unwrap() .mask() .into(), - dhcp: ipv4.dhcp_range.map(|[start, end]| NetworkDHCPXML { + dhcp: ipv4.dhcp.map(|dhcp| NetworkDHCPXML { range: NetworkDHCPRangeXML { - start: IpAddr::V4(start), - end: IpAddr::V4(end), + start: IpAddr::V4(dhcp.start), + end: IpAddr::V4(dhcp.end), }, + hosts: dhcp + .hosts + .into_iter() + .map(|c| NetworkDHCPHostXML { + mac: c.mac, + name: c.name, + ip: c.ip.into(), + }) + .collect::>(), }), }) } @@ -528,11 +564,20 @@ impl NetworkInfo { .unwrap() .mask() .into(), - dhcp: ipv6.dhcp_range.map(|[start, end]| NetworkDHCPXML { + dhcp: ipv6.dhcp.map(|dhcp| NetworkDHCPXML { range: NetworkDHCPRangeXML { - start: IpAddr::V6(start), - end: IpAddr::V6(end), + start: IpAddr::V6(dhcp.start), + end: IpAddr::V6(dhcp.end), }, + hosts: dhcp + .hosts + .into_iter() + .map(|h| NetworkDHCPHostXML { + mac: "".to_string(), + name: h.name, + ip: h.ip.into(), + }) + .collect(), }), }) } @@ -588,10 +633,19 @@ impl NetworkInfo { as u32, p => p, }, - dhcp_range: i - .dhcp - .as_ref() - .map(|d| [extract_ipv4(d.range.start), extract_ipv4(d.range.end)]), + dhcp: i.dhcp.as_ref().map(|d| IPv4DHCPConfig { + start: extract_ipv4(d.range.start), + end: extract_ipv4(d.range.end), + hosts: d + .hosts + .iter() + .map(|h| DHCPv4HostReservation { + mac: h.mac.to_string(), + name: h.name.to_string(), + ip: extract_ipv4(h.ip), + }) + .collect(), + }), }), ip_v6: xml .ips @@ -605,10 +659,18 @@ impl NetworkInfo { as u32, p => p, }, - dhcp_range: i - .dhcp - .as_ref() - .map(|d| [extract_ipv6(d.range.start), extract_ipv6(d.range.end)]), + dhcp: i.dhcp.as_ref().map(|d| IPv6DHCPConfig { + start: extract_ipv6(d.range.start), + end: extract_ipv6(d.range.end), + hosts: d + .hosts + .iter() + .map(|h| DHCPv6HostReservation { + name: h.name.to_string(), + ip: extract_ipv6(h.ip), + }) + .collect(), + }), }), }) } diff --git a/virtweb_frontend/src/api/NetworksApi.ts b/virtweb_frontend/src/api/NetworksApi.ts index 9b76cde..4ceadaf 100644 --- a/virtweb_frontend/src/api/NetworksApi.ts +++ b/virtweb_frontend/src/api/NetworksApi.ts @@ -1,9 +1,22 @@ import { APIClient } from "./ApiClient"; +export interface DHCPHost { + // This field is unspecified in IPv6 configurations + mac: string | undefined; + name: string; + ip: string; +} + +export interface DHCPConfig { + start: string; + end: string; + hosts: DHCPHost[]; +} + export interface IpConfig { bridge_address: string; prefix: number; - dhcp_range?: [string, string]; + dhcp?: DHCPConfig; } export interface NetworkInfo { diff --git a/virtweb_frontend/src/api/ServerApi.ts b/virtweb_frontend/src/api/ServerApi.ts index 07e6531..22a0016 100644 --- a/virtweb_frontend/src/api/ServerApi.ts +++ b/virtweb_frontend/src/api/ServerApi.ts @@ -19,6 +19,7 @@ export interface ServerConstraints { disk_size: LenConstraint; net_name_size: LenConstraint; net_title_size: LenConstraint; + dhcp_reservation_host_name: LenConstraint; } export interface LenConstraint { diff --git a/virtweb_frontend/src/widgets/net/DHCPHostReservations.tsx b/virtweb_frontend/src/widgets/net/DHCPHostReservations.tsx new file mode 100644 index 0000000..9084b89 --- /dev/null +++ b/virtweb_frontend/src/widgets/net/DHCPHostReservations.tsx @@ -0,0 +1,141 @@ +import { mdiIp } from "@mdi/js"; +import Icon from "@mdi/react"; +import DeleteIcon from "@mui/icons-material/Delete"; +import { + Avatar, + Button, + IconButton, + ListItem, + ListItemAvatar, + ListItemText, + Paper, + Tooltip, +} from "@mui/material"; +import { DHCPConfig, DHCPHost } from "../../api/NetworksApi"; +import { ServerApi } from "../../api/ServerApi"; +import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider"; +import { IPInput } from "../forms/IPInput"; +import { MACInput } from "../forms/MACInput"; +import { TextInput } from "../forms/TextInput"; + +export function DHCPHostReservations(p: { + editable: boolean; + dhcp: DHCPConfig; + version: 4 | 6; + onChange?: (d: DHCPConfig) => void; +}): React.ReactElement { + const addHost = () => { + p.dhcp.hosts.push({ + ip: p.version === 4 ? "192.168.1.30" : "fd00::b200", + name: "host", + mac: p.version === 4 ? "00:00:00:00:00:00" : undefined, + }); + p.onChange?.(p.dhcp); + }; + + return ( + <> + {p.dhcp.hosts.map((h, num) => ( + { + p.onChange?.(p.dhcp); + }} + host={h} + onRemove={() => { + p.dhcp.hosts.splice(num, 1); + p.onChange?.(p.dhcp); + }} + /> + ))} + + {p.editable && ( + + )} + + ); +} + +function HostReservationWidget(p: { + editable: boolean; + host: DHCPHost; + version: 4 | 6; + onChange: () => void; + onRemove: () => void; +}): React.ReactElement { + const confirm = useConfirm(); + const deleteReservation = async () => { + if ( + !(await confirm("Do you really want to remove this host IP reservation?")) + ) + return; + + p.onRemove(); + }; + + return ( + + + + + + + ) + } + > + + + + + + { + p.host.name = v!; + p.onChange(); + }} + type="text" + size={ServerApi.Config.constraints.dhcp_reservation_host_name} + /> + } + /> + +
+ {p.version === 4 && ( + { + p.host.mac = v!; + p.onChange?.(); + }} + /> + )} + + { + p.host.ip = v!; + p.onChange?.(); + }} + /> +
+
+ ); +} diff --git a/virtweb_frontend/src/widgets/net/NetworkDetails.tsx b/virtweb_frontend/src/widgets/net/NetworkDetails.tsx index cab22a8..1b71aca 100644 --- a/virtweb_frontend/src/widgets/net/NetworkDetails.tsx +++ b/virtweb_frontend/src/widgets/net/NetworkDetails.tsx @@ -1,4 +1,4 @@ -import { Checkbox, Grid } from "@mui/material"; +import { Checkbox, Grid, Paper } from "@mui/material"; import React from "react"; import { IpConfig, NetworkApi, NetworkInfo } from "../../api/NetworksApi"; import { ServerApi } from "../../api/ServerApi"; @@ -10,6 +10,7 @@ import { TextInput } from "../forms/TextInput"; import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider"; import { CheckboxInput } from "../forms/CheckboxInput"; import { ResAutostartInput } from "../forms/ResAutostartInput"; +import { DHCPHostReservations } from "./DHCPHostReservations"; interface DetailsProps { net: NetworkInfo; @@ -239,44 +240,59 @@ function IPSection(p: { /> { if (v) - p.config!.dhcp_range = + p.config!.dhcp = p.version === 4 - ? ["192.168.1.100", "192.168.1.200"] - : ["fd00::100", "fd00::f00"]; - else p.config!.dhcp_range = undefined; + ? { + start: "192.168.1.100", + end: "192.168.1.200", + hosts: [], + } + : { start: "fd00::100", end: "fd00::f00", hosts: [] }; + else p.config!.dhcp = undefined; p.onChange?.(p.config); }} /> )} - {p.config?.dhcp_range && ( + {p.config?.dhcp && ( <> - { - p.config!.dhcp_range![0] = v!; - p.onChange(p.config); - }} - /> - { - p.config!.dhcp_range![1] = v!; - p.onChange(p.config); - }} - /> + + { + p.config!.dhcp!.start = v!; + p.onChange(p.config); + }} + /> + { + p.config!.dhcp!.end = v!; + p.onChange(p.config); + }} + /> + + { + p.config!.dhcp = d; + p.onChange?.(p.config); + }} + /> + )}