diff --git a/virtweb_backend/src/libvirt_lib_structures.rs b/virtweb_backend/src/libvirt_lib_structures.rs index b78890e..aaeeb29 100644 --- a/virtweb_backend/src/libvirt_lib_structures.rs +++ b/virtweb_backend/src/libvirt_lib_structures.rs @@ -63,11 +63,20 @@ pub struct FeaturesXML { #[serde(rename = "acpi")] pub struct ACPIXML {} +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "source")] +pub struct NetIntSourceXML { + #[serde(rename(serialize = "@network"))] + pub network: String, +} + #[derive(serde::Serialize, serde::Deserialize)] #[serde(rename = "interface")] pub struct DomainNetInterfaceXML { #[serde(rename(serialize = "@type"))] pub r#type: String, + + pub source: Option, } /// Devices information diff --git a/virtweb_backend/src/libvirt_rest_structures.rs b/virtweb_backend/src/libvirt_rest_structures.rs index c9d2404..eb098e5 100644 --- a/virtweb_backend/src/libvirt_rest_structures.rs +++ b/virtweb_backend/src/libvirt_rest_structures.rs @@ -3,7 +3,7 @@ use crate::constants; use crate::libvirt_lib_structures::{ DevicesXML, DiskBootXML, DiskDriverXML, DiskReadOnlyXML, DiskSourceXML, DiskTargetXML, DiskXML, DomainCPUTopology, DomainCPUXML, DomainMemoryXML, DomainNetInterfaceXML, DomainVCPUXML, - DomainXML, FeaturesXML, GraphicsXML, NetworkDHCPRangeXML, NetworkDHCPXML, + DomainXML, FeaturesXML, GraphicsXML, NetIntSourceXML, NetworkDHCPRangeXML, NetworkDHCPXML, NetworkDNSForwarderXML, NetworkDNSXML, NetworkDomainXML, NetworkForwardXML, NetworkIPXML, NetworkXML, OSLoaderXML, OSTypeXML, XMLUuid, ACPIXML, OSXML, }; @@ -68,7 +68,7 @@ pub enum VMArchitecture { #[serde(tag = "type")] pub enum Network { UserspaceSLIRPStack, - // TODO : complete network types + DefinedNetwork { network: String }, // TODO : complete network types } #[derive(serde::Serialize, serde::Deserialize)] @@ -226,6 +226,11 @@ impl VMInfo { networks.push(match n { Network::UserspaceSLIRPStack => DomainNetInterfaceXML { r#type: "user".to_string(), + source: None, + }, + Network::DefinedNetwork { network } => DomainNetInterfaceXML { + r#type: "network".to_string(), + source: Some(NetIntSourceXML { network }), }, }) } @@ -344,6 +349,9 @@ impl VMInfo { .iter() .map(|d| match d.r#type.as_str() { "user" => Ok(Network::UserspaceSLIRPStack), + "network" => Ok(Network::DefinedNetwork { + network: d.source.as_ref().unwrap().network.to_string(), + }), a => Err(LibVirtStructError::DomainExtraction(format!( "Unknown network interface type: {a}! " ))), diff --git a/virtweb_frontend/src/api/VMApi.ts b/virtweb_frontend/src/api/VMApi.ts index b5a083a..4111af3 100644 --- a/virtweb_frontend/src/api/VMApi.ts +++ b/virtweb_frontend/src/api/VMApi.ts @@ -30,12 +30,17 @@ export interface VMDisk { deleteType?: "keepfile" | "deletefile"; } -export type VMNetInterface = VMNetUserspaceSLIRPStack; +export type VMNetInterface = VMNetUserspaceSLIRPStack | VMNetDefinedNetwork; export interface VMNetUserspaceSLIRPStack { type: "UserspaceSLIRPStack"; } +export interface VMNetDefinedNetwork { + type: "DefinedNetwork"; + network: string; +} + interface VMInfoInterface { name: string; uuid?: string; @@ -65,7 +70,7 @@ export class VMInfo implements VMInfoInterface { vnc_access: boolean; iso_file?: string; disks: VMDisk[]; - networks: VMNetUserspaceSLIRPStack[]; + networks: VMNetInterface[]; constructor(int: VMInfoInterface) { this.name = int.name; diff --git a/virtweb_frontend/src/widgets/forms/VMNetworksList.tsx b/virtweb_frontend/src/widgets/forms/VMNetworksList.tsx index a4e2e32..0b0fb7d 100644 --- a/virtweb_frontend/src/widgets/forms/VMNetworksList.tsx +++ b/virtweb_frontend/src/widgets/forms/VMNetworksList.tsx @@ -13,11 +13,13 @@ import { import { VMInfo, VMNetInterface } from "../../api/VMApi"; import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider"; import { SelectInput } from "./SelectInput"; +import { NetworkInfo } from "../../api/NetworksApi"; export function VMNetworksList(p: { vm: VMInfo; onChange?: () => void; editable: boolean; + networksList: NetworkInfo[]; }): React.ReactElement { const addNew = () => { p.vm.networks.push({ type: "UserspaceSLIRPStack" }); @@ -28,15 +30,14 @@ export function VMNetworksList(p: { <> {/* networks list */} {p.vm.networks.map((n, num) => ( - { p.vm.networks.splice(num, 1); p.onChange?.(); }} + {...p} /> ))} @@ -47,11 +48,12 @@ export function VMNetworksList(p: { ); } -function NetworkInfo(p: { +function NetworkInfoWidget(p: { editable: boolean; network: VMNetInterface; onChange?: () => void; removeFromList: () => void; + networksList: NetworkInfo[]; }): React.ReactElement { const confirm = useConfirm(); const deleteNetwork = async () => { @@ -65,7 +67,7 @@ function NetworkInfo(p: { }; return ( -
+ <> { p.network.type = v as any; + p.onChange?.(); }} options={[ { @@ -103,6 +106,11 @@ function NetworkInfo(p: { description: "Provides a virtual LAN with NAT to the outside world. The virtual network has DHCP & DNS services", }, + { + label: "Defined network", + value: "DefinedNetwork", + description: "Attach to a defined network", + }, ]} /> ) : ( @@ -111,6 +119,32 @@ function NetworkInfo(p: { } /> -
+
+ {p.network.type === "DefinedNetwork" && ( + { + const chars = [n.forward_mode.toString()]; + if (n.ip_v4) chars.push("IPv4"); + if (n.ip_v6) chars.push("IPv6"); + if (n.description) chars.push(n.description); + + return { + label: n.name, + value: n.name, + description: chars.join(" - "), + }; + })} + value={p.network.network} + onValueChange={(v) => { + if (p.network.type === "DefinedNetwork") + p.network.network = v as any; + p.onChange?.(); + }} + /> + )} +
+ ); } diff --git a/virtweb_frontend/src/widgets/vms/VMDetails.tsx b/virtweb_frontend/src/widgets/vms/VMDetails.tsx index 7ff47a0..1e218fd 100644 --- a/virtweb_frontend/src/widgets/vms/VMDetails.tsx +++ b/virtweb_frontend/src/widgets/vms/VMDetails.tsx @@ -14,6 +14,7 @@ import { VMSelectIsoInput } from "../forms/VMSelectIsoInput"; import { VMScreenshot } from "./VMScreenshot"; import { ResAutostartInput } from "../forms/ResAutostartInput"; import { VMNetworksList } from "../forms/VMNetworksList"; +import { NetworkApi, NetworkInfo } from "../../api/NetworksApi"; interface DetailsProps { vm: VMInfo; @@ -27,10 +28,12 @@ export function VMDetails(p: DetailsProps): React.ReactElement { const [vcpuCombinations, setVCPUCombinations] = React.useState< number[] | any >(); + const [networksList, setNetworksList] = React.useState(); const load = async () => { setIsoList(await IsoFilesApi.GetList()); setVCPUCombinations(await ServerApi.NumberVCPUs()); + setNetworksList(await NetworkApi.GetList()); }; return ( @@ -42,6 +45,7 @@ export function VMDetails(p: DetailsProps): React.ReactElement { )} @@ -50,7 +54,11 @@ export function VMDetails(p: DetailsProps): React.ReactElement { } function VMDetailsInner( - p: DetailsProps & { isoList: IsoFile[]; vcpuCombinations: number[] } + p: DetailsProps & { + isoList: IsoFile[]; + vcpuCombinations: number[]; + networksList: NetworkInfo[]; + } ): React.ReactElement { return ( @@ -201,12 +209,12 @@ function VMDetailsInner( p.onChange?.(); }} /> - + {/* Networks section */} - + );