Can define IP reservations for networks

This commit is contained in:
Pierre HUBERT 2023-12-19 17:57:38 +01:00
parent ed1bd806d7
commit afebe97395
7 changed files with 324 additions and 48 deletions

View File

@ -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 },
},
})
}

View File

@ -432,10 +432,49 @@ pub struct NetworkIPXML {
pub dhcp: Option<NetworkDHCPXML>,
}
impl NetworkIPXML {
pub fn into_xml(mut self) -> anyhow::Result<String> {
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("<host").unwrap();
hosts_xml.push(host_xml[start_offset..].to_string());
}
dhcp.hosts = vec![];
}
let mut res = serde_xml_rs::to_string(&self)?;
let hosts_xml = hosts_xml.join("\n");
res = res.replace("</dhcp>", &format!("{hosts_xml}</dhcp>"));
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<NetworkDHCPHostXML>,
}
#[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("<ip").unwrap();
ips_xml.push(ip_xml[start_offset..].to_string());
}

View File

@ -4,9 +4,9 @@ use crate::libvirt_lib_structures::{
DevicesXML, DiskBootXML, DiskDriverXML, DiskReadOnlyXML, DiskSourceXML, DiskTargetXML, DiskXML,
DomainCPUTopology, DomainCPUXML, DomainInputXML, DomainMemoryXML, DomainNetInterfaceXML,
DomainVCPUXML, DomainXML, FeaturesXML, GraphicsXML, NetIntSourceXML, NetMacAddress,
NetworkDHCPRangeXML, NetworkDHCPXML, NetworkDNSForwarderXML, NetworkDNSXML, NetworkDomainXML,
NetworkForwardXML, NetworkIPXML, NetworkXML, OSLoaderXML, OSTypeXML, TPMBackendXML,
TPMDeviceXML, VideoModelXML, VideoXML, XMLUuid, ACPIXML, OSXML,
NetworkDHCPHostXML, NetworkDHCPRangeXML, NetworkDHCPXML, NetworkDNSForwarderXML, NetworkDNSXML,
NetworkDomainXML, NetworkForwardXML, NetworkIPXML, NetworkXML, OSLoaderXML, OSTypeXML,
TPMBackendXML, TPMDeviceXML, VideoModelXML, VideoXML, XMLUuid, ACPIXML, OSXML,
};
use crate::libvirt_rest_structures::LibVirtStructError::StructureExtraction;
use crate::utils::disks_utils::Disk;
@ -442,18 +442,45 @@ pub enum NetworkForwardMode {
Isolated,
}
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
pub struct DHCPv4HostReservation {
mac: String,
name: String,
ip: Ipv4Addr,
}
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
pub struct IPv4DHCPConfig {
start: Ipv4Addr,
end: Ipv4Addr,
hosts: Vec<DHCPv4HostReservation>,
}
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
pub struct IPV4Config {
bridge_address: Ipv4Addr,
prefix: u32,
dhcp_range: Option<[Ipv4Addr; 2]>,
dhcp: Option<IPv4DHCPConfig>,
}
#[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<DHCPv6HostReservation>,
}
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
pub struct IPV6Config {
bridge_address: Ipv6Addr,
prefix: u32,
dhcp_range: Option<[Ipv6Addr; 2]>,
dhcp: Option<IPv6DHCPConfig>,
}
/// 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::<Vec<_>>(),
}),
})
}
@ -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(),
}),
}),
})
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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) => (
<HostReservationWidget
key={num}
{...p}
onChange={() => {
p.onChange?.(p.dhcp);
}}
host={h}
onRemove={() => {
p.dhcp.hosts.splice(num, 1);
p.onChange?.(p.dhcp);
}}
/>
))}
{p.editable && (
<Button onClick={addHost}>Add new host reservation</Button>
)}
</>
);
}
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 (
<Paper elevation={3} style={{ padding: "10px", marginTop: "20px" }}>
<ListItem
secondaryAction={
p.editable && (
<IconButton
edge="end"
aria-label="remove network"
onClick={deleteReservation}
>
<Tooltip title="Remove host IP allocation">
<DeleteIcon />
</Tooltip>
</IconButton>
)
}
>
<ListItemAvatar>
<Avatar>
<Icon path={mdiIp} />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<TextInput
editable={p.editable}
label="Host name"
value={p.host.name}
onValueChange={(v) => {
p.host.name = v!;
p.onChange();
}}
type="text"
size={ServerApi.Config.constraints.dhcp_reservation_host_name}
/>
}
/>
</ListItem>
<div style={{ marginLeft: "70px" }}>
{p.version === 4 && (
<MACInput
editable={p.editable}
label="MAC Address"
value={p.host.mac}
onValueChange={(v) => {
p.host.mac = v!;
p.onChange?.();
}}
/>
)}
<IPInput
editable={p.editable}
label="IP address"
version={p.version}
value={p.host.ip}
onValueChange={(v) => {
p.host.ip = v!;
p.onChange?.();
}}
/>
</div>
</Paper>
);
}

View File

@ -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: {
/>
<CheckboxInput
checked={!!p.config.dhcp_range}
checked={!!p.config.dhcp}
editable={p.editable}
label="Enable DHCP"
onValueChange={(v) => {
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 && (
<>
<IPInput
label="DHCP allocation start"
editable={p.editable}
version={p.version}
value={p.config.dhcp_range[0]}
onValueChange={(v) => {
p.config!.dhcp_range![0] = v!;
p.onChange(p.config);
}}
/>
<IPInput
label="DHCP allocation end"
editable={p.editable}
version={p.version}
value={p.config.dhcp_range[1]}
onValueChange={(v) => {
p.config!.dhcp_range![1] = v!;
p.onChange(p.config);
}}
/>
<Paper elevation={3} style={{ padding: "10px" }}>
<IPInput
label="DHCP allocation start"
editable={p.editable}
version={p.version}
value={p.config.dhcp.start}
onValueChange={(v) => {
p.config!.dhcp!.start = v!;
p.onChange(p.config);
}}
/>
<IPInput
label="DHCP allocation end"
editable={p.editable}
version={p.version}
value={p.config.dhcp.end}
onValueChange={(v) => {
p.config!.dhcp!.end = v!;
p.onChange(p.config);
}}
/>
<DHCPHostReservations
{...p}
dhcp={p.config.dhcp}
onChange={(d) => {
p.config!.dhcp = d;
p.onChange?.(p.config);
}}
/>
</Paper>
</>
)}
</EditSection>