Can create NAT networks

This commit is contained in:
Pierre HUBERT 2023-12-07 20:03:11 +01:00
parent 5f0f56a9f9
commit 54a3013c59
7 changed files with 210 additions and 12 deletions

View File

@ -107,17 +107,24 @@ impl Handler<DefineDomainReq> for LibVirtActor {
type Result = anyhow::Result<XMLUuid>; type Result = anyhow::Result<XMLUuid>;
fn handle(&mut self, mut msg: DefineDomainReq, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, mut msg: DefineDomainReq, _ctx: &mut Self::Context) -> Self::Result {
// A issue with the disks definition serialization needs them to be serialized aside // A issue with the disks & network interface definition serialization needs them to be serialized aside
let mut disks_xml = Vec::with_capacity(msg.0.devices.disks.len()); let mut devices_xml = Vec::with_capacity(msg.0.devices.disks.len());
for disk in msg.0.devices.disks { for disk in msg.0.devices.disks {
let disk_xml = serde_xml_rs::to_string(&disk)?; let disk_xml = serde_xml_rs::to_string(&disk)?;
let start_offset = disk_xml.find("<disk").unwrap(); let start_offset = disk_xml.find("<disk").unwrap();
disks_xml.push(disk_xml[start_offset..].to_string()); devices_xml.push(disk_xml[start_offset..].to_string());
} }
for network in msg.0.devices.net_interfaces {
let network_xml = serde_xml_rs::to_string(&network)?;
let start_offset = network_xml.find("<interface").unwrap();
devices_xml.push(network_xml[start_offset..].to_string());
}
msg.0.devices.disks = vec![]; msg.0.devices.disks = vec![];
msg.0.devices.net_interfaces = vec![];
let mut xml = serde_xml_rs::to_string(&msg.0)?; let mut xml = serde_xml_rs::to_string(&msg.0)?;
let disks_xml = disks_xml.join("\n"); let disks_xml = devices_xml.join("\n");
xml = xml.replacen("<devices>", &format!("<devices>{disks_xml}"), 1); xml = xml.replacen("<devices>", &format!("<devices>{disks_xml}"), 1);
log::debug!("Define domain:\n{}", xml); log::debug!("Define domain:\n{}", xml);

View File

@ -63,6 +63,13 @@ pub struct FeaturesXML {
#[serde(rename = "acpi")] #[serde(rename = "acpi")]
pub struct ACPIXML {} pub struct ACPIXML {}
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "interface")]
pub struct DomainNetInterfaceXML {
#[serde(rename(serialize = "@type"))]
pub r#type: String,
}
/// Devices information /// Devices information
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "devices")] #[serde(rename = "devices")]
@ -74,6 +81,10 @@ pub struct DevicesXML {
/// Disks (used for storage) /// Disks (used for storage)
#[serde(default, rename = "disk", skip_serializing_if = "Vec::is_empty")] #[serde(default, rename = "disk", skip_serializing_if = "Vec::is_empty")]
pub disks: Vec<DiskXML>, pub disks: Vec<DiskXML>,
/// Networks cards
#[serde(default, rename = "interface", skip_serializing_if = "Vec::is_empty")]
pub net_interfaces: Vec<DomainNetInterfaceXML>,
} }
/// Screen information /// Screen information
@ -276,7 +287,7 @@ pub struct NetworkDNSXML {
/// Network DNS information /// Network DNS information
#[derive(serde::Serialize, serde::Deserialize, Debug)] #[derive(serde::Serialize, serde::Deserialize, Debug)]
#[serde(rename = "fowarder")] #[serde(rename = "forwarder")]
pub struct NetworkDNSForwarderXML { pub struct NetworkDNSForwarderXML {
/// Address of the DNS server /// Address of the DNS server
#[serde(rename(serialize = "@addr"))] #[serde(rename(serialize = "@addr"))]

View File

@ -2,10 +2,10 @@ use crate::app_config::AppConfig;
use crate::constants; use crate::constants;
use crate::libvirt_lib_structures::{ use crate::libvirt_lib_structures::{
DevicesXML, DiskBootXML, DiskDriverXML, DiskReadOnlyXML, DiskSourceXML, DiskTargetXML, DiskXML, DevicesXML, DiskBootXML, DiskDriverXML, DiskReadOnlyXML, DiskSourceXML, DiskTargetXML, DiskXML,
DomainCPUTopology, DomainCPUXML, DomainMemoryXML, DomainVCPUXML, DomainXML, FeaturesXML, DomainCPUTopology, DomainCPUXML, DomainMemoryXML, DomainNetInterfaceXML, DomainVCPUXML,
GraphicsXML, NetworkDHCPRangeXML, NetworkDHCPXML, NetworkDNSForwarderXML, NetworkDNSXML, DomainXML, FeaturesXML, GraphicsXML, NetworkDHCPRangeXML, NetworkDHCPXML,
NetworkDomainXML, NetworkForwardXML, NetworkIPXML, NetworkXML, OSLoaderXML, OSTypeXML, XMLUuid, NetworkDNSForwarderXML, NetworkDNSXML, NetworkDomainXML, NetworkForwardXML, NetworkIPXML,
ACPIXML, OSXML, NetworkXML, OSLoaderXML, OSTypeXML, XMLUuid, ACPIXML, OSXML,
}; };
use crate::libvirt_rest_structures::LibVirtStructError::StructureExtraction; use crate::libvirt_rest_structures::LibVirtStructError::StructureExtraction;
use crate::utils::disks_utils::Disk; use crate::utils::disks_utils::Disk;
@ -64,6 +64,13 @@ pub enum VMArchitecture {
X86_64, X86_64,
} }
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(tag = "type")]
pub enum Network {
UserspaceSLIRPStack,
// TODO : complete network types
}
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
pub struct VMInfo { pub struct VMInfo {
/// VM name (alphanumeric characters only) /// VM name (alphanumeric characters only)
@ -84,7 +91,8 @@ pub struct VMInfo {
pub iso_file: Option<String>, pub iso_file: Option<String>,
/// Storage - https://access.redhat.com/documentation/fr-fr/red_hat_enterprise_linux/6/html/virtualization_administration_guide/sect-virtualization-virtualized_block_devices-adding_storage_devices_to_guests#sect-Virtualization-Adding_storage_devices_to_guests-Adding_file_based_storage_to_a_guest /// Storage - https://access.redhat.com/documentation/fr-fr/red_hat_enterprise_linux/6/html/virtualization_administration_guide/sect-virtualization-virtualized_block_devices-adding_storage_devices_to_guests#sect-Virtualization-Adding_storage_devices_to_guests-Adding_file_based_storage_to_a_guest
pub disks: Vec<Disk>, pub disks: Vec<Disk>,
// TODO : network interfaces /// Network cards
pub networks: Vec<Network>,
} }
impl VMInfo { impl VMInfo {
@ -213,6 +221,15 @@ impl VMInfo {
}) })
} }
let mut networks = vec![];
for n in self.networks {
networks.push(match n {
Network::UserspaceSLIRPStack => DomainNetInterfaceXML {
r#type: "user".to_string(),
},
})
}
Ok(DomainXML { Ok(DomainXML {
r#type: "kvm".to_string(), r#type: "kvm".to_string(),
name: self.name, name: self.name,
@ -245,6 +262,7 @@ impl VMInfo {
devices: DevicesXML { devices: DevicesXML {
graphics: vnc_graphics, graphics: vnc_graphics,
disks, disks,
net_interfaces: networks,
}, },
memory: DomainMemoryXML { memory: DomainMemoryXML {
@ -319,6 +337,18 @@ impl VMInfo {
.filter(|d| d.device == "disk") .filter(|d| d.device == "disk")
.map(|d| Disk::load_from_file(&d.source.file).unwrap()) .map(|d| Disk::load_from_file(&d.source.file).unwrap())
.collect(), .collect(),
networks: domain
.devices
.net_interfaces
.iter()
.map(|d| match d.r#type.as_str() {
"user" => Ok(Network::UserspaceSLIRPStack),
a => Err(LibVirtStructError::DomainExtraction(format!(
"Unknown network interface type: {a}! "
))),
})
.collect::<Result<Vec<_>, _>>()?,
}) })
} }
} }

View File

@ -30,6 +30,12 @@ export interface VMDisk {
deleteType?: "keepfile" | "deletefile"; deleteType?: "keepfile" | "deletefile";
} }
export type VMNetInterface = VMNetUserspaceSLIRPStack;
export interface VMNetUserspaceSLIRPStack {
type: "UserspaceSLIRPStack";
}
interface VMInfoInterface { interface VMInfoInterface {
name: string; name: string;
uuid?: string; uuid?: string;
@ -43,6 +49,7 @@ interface VMInfoInterface {
vnc_access: boolean; vnc_access: boolean;
iso_file?: string; iso_file?: string;
disks: VMDisk[]; disks: VMDisk[];
networks: VMNetInterface[];
} }
export class VMInfo implements VMInfoInterface { export class VMInfo implements VMInfoInterface {
@ -58,6 +65,7 @@ export class VMInfo implements VMInfoInterface {
vnc_access: boolean; vnc_access: boolean;
iso_file?: string; iso_file?: string;
disks: VMDisk[]; disks: VMDisk[];
networks: VMNetUserspaceSLIRPStack[];
constructor(int: VMInfoInterface) { constructor(int: VMInfoInterface) {
this.name = int.name; this.name = int.name;
@ -72,6 +80,7 @@ export class VMInfo implements VMInfoInterface {
this.vnc_access = int.vnc_access; this.vnc_access = int.vnc_access;
this.iso_file = int.iso_file; this.iso_file = int.iso_file;
this.disks = int.disks; this.disks = int.disks;
this.networks = int.networks;
} }
static NewEmpty(): VMInfo { static NewEmpty(): VMInfo {
@ -83,6 +92,7 @@ export class VMInfo implements VMInfoInterface {
number_vcpu: 1, number_vcpu: 1,
vnc_access: true, vnc_access: true,
disks: [], disks: [],
networks: [],
}); });
} }

View File

@ -1,9 +1,16 @@
import { FormControl, InputLabel, MenuItem, Select } from "@mui/material"; import {
FormControl,
InputLabel,
MenuItem,
Select,
Typography,
} from "@mui/material";
import { TextInput } from "./TextInput"; import { TextInput } from "./TextInput";
export interface SelectOption { export interface SelectOption {
value?: string; value?: string;
label: string; label: string;
description?: string;
} }
export function SelectInput(p: { export function SelectInput(p: {
@ -33,7 +40,18 @@ export function SelectInput(p: {
value={e.value} value={e.value}
style={{ fontStyle: e.value === undefined ? "italic" : undefined }} style={{ fontStyle: e.value === undefined ? "italic" : undefined }}
> >
<div>
{e.label} {e.label}
{e.description && (
<Typography
component={"div"}
variant="caption"
style={{ whiteSpace: "normal" }}
>
{e.description}
</Typography>
)}
</div>
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>

View File

@ -0,0 +1,116 @@
import { mdiNetworkOutline } from "@mdi/js";
import Icon from "@mdi/react";
import DeleteIcon from "@mui/icons-material/Delete";
import {
Avatar,
Button,
IconButton,
ListItem,
ListItemAvatar,
ListItemText,
Tooltip,
} from "@mui/material";
import { VMInfo, VMNetInterface } from "../../api/VMApi";
import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
import { SelectInput } from "./SelectInput";
export function VMNetworksList(p: {
vm: VMInfo;
onChange?: () => void;
editable: boolean;
}): React.ReactElement {
const addNew = () => {
p.vm.networks.push({ type: "UserspaceSLIRPStack" });
p.onChange?.();
};
return (
<>
{/* networks list */}
{p.vm.networks.map((n, num) => (
<NetworkInfo
key={num}
editable={p.editable}
network={n}
onChange={p.onChange}
removeFromList={() => {
p.vm.networks.splice(num, 1);
p.onChange?.();
}}
/>
))}
{p.editable && (
<Button onClick={addNew}>Add a new network interface</Button>
)}
</>
);
}
function NetworkInfo(p: {
editable: boolean;
network: VMNetInterface;
onChange?: () => void;
removeFromList: () => void;
}): React.ReactElement {
const confirm = useConfirm();
const deleteNetwork = async () => {
if (
!(await confirm("Do you really want to remove this network interface?"))
)
return;
p.removeFromList();
p.onChange?.();
};
return (
<div>
<ListItem
secondaryAction={
p.editable && (
<IconButton
edge="end"
aria-label="remove network"
onClick={deleteNetwork}
>
<Tooltip title="Remove network">
<DeleteIcon />
</Tooltip>
</IconButton>
)
}
>
<ListItemAvatar>
<Avatar>
<Icon path={mdiNetworkOutline} />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
p.editable ? (
<SelectInput
label=""
editable
value={p.network.type}
onValueChange={(v) => {
p.network.type = v as any;
}}
options={[
{
label: "Userspace SLIRP stack",
value: "UserspaceSLIRPStack",
description:
"Provides a virtual LAN with NAT to the outside world. The virtual network has DHCP & DNS services",
},
]}
/>
) : (
p.network.type
)
}
/>
</ListItem>
</div>
);
}

View File

@ -13,6 +13,7 @@ import { VMDisksList } from "../forms/VMDisksList";
import { VMSelectIsoInput } from "../forms/VMSelectIsoInput"; import { VMSelectIsoInput } from "../forms/VMSelectIsoInput";
import { VMScreenshot } from "./VMScreenshot"; import { VMScreenshot } from "./VMScreenshot";
import { ResAutostartInput } from "../forms/ResAutostartInput"; import { ResAutostartInput } from "../forms/ResAutostartInput";
import { VMNetworksList } from "../forms/VMNetworksList";
interface DetailsProps { interface DetailsProps {
vm: VMInfo; vm: VMInfo;
@ -202,6 +203,11 @@ function VMDetailsInner(
/> />
<VMDisksList vm={p.vm} editable={p.editable} onChange={p.onChange} /> <VMDisksList vm={p.vm} editable={p.editable} onChange={p.onChange} />
</EditSection> </EditSection>
{/* Networks section */}
<EditSection title="Networks">
<VMNetworksList vm={p.vm} editable={p.editable} onChange={p.onChange} />
</EditSection>
</Grid> </Grid>
); );
} }