diff --git a/virtweb_backend/src/actors/libvirt_actor.rs b/virtweb_backend/src/actors/libvirt_actor.rs index 474422f..9ebea48 100644 --- a/virtweb_backend/src/actors/libvirt_actor.rs +++ b/virtweb_backend/src/actors/libvirt_actor.rs @@ -107,17 +107,24 @@ impl Handler for LibVirtActor { type Result = anyhow::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 - let mut disks_xml = Vec::with_capacity(msg.0.devices.disks.len()); + // A issue with the disks & network interface definition serialization needs them to be serialized aside + let mut devices_xml = Vec::with_capacity(msg.0.devices.disks.len()); for disk in msg.0.devices.disks { let disk_xml = serde_xml_rs::to_string(&disk)?; let start_offset = disk_xml.find("", &format!("{disks_xml}"), 1); log::debug!("Define domain:\n{}", xml); diff --git a/virtweb_backend/src/libvirt_lib_structures.rs b/virtweb_backend/src/libvirt_lib_structures.rs index 98a2832..b78890e 100644 --- a/virtweb_backend/src/libvirt_lib_structures.rs +++ b/virtweb_backend/src/libvirt_lib_structures.rs @@ -63,6 +63,13 @@ pub struct FeaturesXML { #[serde(rename = "acpi")] pub struct ACPIXML {} +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "interface")] +pub struct DomainNetInterfaceXML { + #[serde(rename(serialize = "@type"))] + pub r#type: String, +} + /// Devices information #[derive(serde::Serialize, serde::Deserialize)] #[serde(rename = "devices")] @@ -74,6 +81,10 @@ pub struct DevicesXML { /// Disks (used for storage) #[serde(default, rename = "disk", skip_serializing_if = "Vec::is_empty")] pub disks: Vec, + + /// Networks cards + #[serde(default, rename = "interface", skip_serializing_if = "Vec::is_empty")] + pub net_interfaces: Vec, } /// Screen information @@ -276,7 +287,7 @@ pub struct NetworkDNSXML { /// Network DNS information #[derive(serde::Serialize, serde::Deserialize, Debug)] -#[serde(rename = "fowarder")] +#[serde(rename = "forwarder")] pub struct NetworkDNSForwarderXML { /// Address of the DNS server #[serde(rename(serialize = "@addr"))] diff --git a/virtweb_backend/src/libvirt_rest_structures.rs b/virtweb_backend/src/libvirt_rest_structures.rs index ba71554..c9d2404 100644 --- a/virtweb_backend/src/libvirt_rest_structures.rs +++ b/virtweb_backend/src/libvirt_rest_structures.rs @@ -2,10 +2,10 @@ use crate::app_config::AppConfig; use crate::constants; use crate::libvirt_lib_structures::{ DevicesXML, DiskBootXML, DiskDriverXML, DiskReadOnlyXML, DiskSourceXML, DiskTargetXML, DiskXML, - DomainCPUTopology, DomainCPUXML, DomainMemoryXML, DomainVCPUXML, DomainXML, FeaturesXML, - GraphicsXML, NetworkDHCPRangeXML, NetworkDHCPXML, NetworkDNSForwarderXML, NetworkDNSXML, - NetworkDomainXML, NetworkForwardXML, NetworkIPXML, NetworkXML, OSLoaderXML, OSTypeXML, XMLUuid, - ACPIXML, OSXML, + DomainCPUTopology, DomainCPUXML, DomainMemoryXML, DomainNetInterfaceXML, DomainVCPUXML, + DomainXML, FeaturesXML, GraphicsXML, NetworkDHCPRangeXML, NetworkDHCPXML, + NetworkDNSForwarderXML, NetworkDNSXML, NetworkDomainXML, NetworkForwardXML, NetworkIPXML, + NetworkXML, OSLoaderXML, OSTypeXML, XMLUuid, ACPIXML, OSXML, }; use crate::libvirt_rest_structures::LibVirtStructError::StructureExtraction; use crate::utils::disks_utils::Disk; @@ -64,6 +64,13 @@ pub enum VMArchitecture { X86_64, } +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(tag = "type")] +pub enum Network { + UserspaceSLIRPStack, + // TODO : complete network types +} + #[derive(serde::Serialize, serde::Deserialize)] pub struct VMInfo { /// VM name (alphanumeric characters only) @@ -84,7 +91,8 @@ pub struct VMInfo { pub iso_file: Option, /// 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, - // TODO : network interfaces + /// Network cards + pub networks: Vec, } 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 { r#type: "kvm".to_string(), name: self.name, @@ -245,6 +262,7 @@ impl VMInfo { devices: DevicesXML { graphics: vnc_graphics, disks, + net_interfaces: networks, }, memory: DomainMemoryXML { @@ -319,6 +337,18 @@ impl VMInfo { .filter(|d| d.device == "disk") .map(|d| Disk::load_from_file(&d.source.file).unwrap()) .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::, _>>()?, }) } } diff --git a/virtweb_frontend/src/api/VMApi.ts b/virtweb_frontend/src/api/VMApi.ts index 97d7e54..b5a083a 100644 --- a/virtweb_frontend/src/api/VMApi.ts +++ b/virtweb_frontend/src/api/VMApi.ts @@ -30,6 +30,12 @@ export interface VMDisk { deleteType?: "keepfile" | "deletefile"; } +export type VMNetInterface = VMNetUserspaceSLIRPStack; + +export interface VMNetUserspaceSLIRPStack { + type: "UserspaceSLIRPStack"; +} + interface VMInfoInterface { name: string; uuid?: string; @@ -43,6 +49,7 @@ interface VMInfoInterface { vnc_access: boolean; iso_file?: string; disks: VMDisk[]; + networks: VMNetInterface[]; } export class VMInfo implements VMInfoInterface { @@ -58,6 +65,7 @@ export class VMInfo implements VMInfoInterface { vnc_access: boolean; iso_file?: string; disks: VMDisk[]; + networks: VMNetUserspaceSLIRPStack[]; constructor(int: VMInfoInterface) { this.name = int.name; @@ -72,6 +80,7 @@ export class VMInfo implements VMInfoInterface { this.vnc_access = int.vnc_access; this.iso_file = int.iso_file; this.disks = int.disks; + this.networks = int.networks; } static NewEmpty(): VMInfo { @@ -83,6 +92,7 @@ export class VMInfo implements VMInfoInterface { number_vcpu: 1, vnc_access: true, disks: [], + networks: [], }); } diff --git a/virtweb_frontend/src/widgets/forms/SelectInput.tsx b/virtweb_frontend/src/widgets/forms/SelectInput.tsx index dc0d78a..87f8297 100644 --- a/virtweb_frontend/src/widgets/forms/SelectInput.tsx +++ b/virtweb_frontend/src/widgets/forms/SelectInput.tsx @@ -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"; export interface SelectOption { value?: string; label: string; + description?: string; } export function SelectInput(p: { @@ -33,7 +40,18 @@ export function SelectInput(p: { value={e.value} style={{ fontStyle: e.value === undefined ? "italic" : undefined }} > - {e.label} +
+ {e.label} + {e.description && ( + + {e.description} + + )} +
))} diff --git a/virtweb_frontend/src/widgets/forms/VMNetworksList.tsx b/virtweb_frontend/src/widgets/forms/VMNetworksList.tsx new file mode 100644 index 0000000..a4e2e32 --- /dev/null +++ b/virtweb_frontend/src/widgets/forms/VMNetworksList.tsx @@ -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) => ( + { + p.vm.networks.splice(num, 1); + p.onChange?.(); + }} + /> + ))} + + {p.editable && ( + + )} + + ); +} + +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 ( +
+ + + + + + ) + } + > + + + + + + { + 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 + ) + } + /> + +
+ ); +} diff --git a/virtweb_frontend/src/widgets/vms/VMDetails.tsx b/virtweb_frontend/src/widgets/vms/VMDetails.tsx index 7574d5e..7ff47a0 100644 --- a/virtweb_frontend/src/widgets/vms/VMDetails.tsx +++ b/virtweb_frontend/src/widgets/vms/VMDetails.tsx @@ -13,6 +13,7 @@ import { VMDisksList } from "../forms/VMDisksList"; import { VMSelectIsoInput } from "../forms/VMSelectIsoInput"; import { VMScreenshot } from "./VMScreenshot"; import { ResAutostartInput } from "../forms/ResAutostartInput"; +import { VMNetworksList } from "../forms/VMNetworksList"; interface DetailsProps { vm: VMInfo; @@ -202,6 +203,11 @@ function VMDetailsInner( /> + + {/* Networks section */} + + + ); }