Can create NAT networks
This commit is contained in:
parent
5f0f56a9f9
commit
54a3013c59
@ -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);
|
||||||
|
@ -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"))]
|
||||||
|
@ -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<_>, _>>()?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 }}
|
||||||
>
|
>
|
||||||
{e.label}
|
<div>
|
||||||
|
{e.label}
|
||||||
|
{e.description && (
|
||||||
|
<Typography
|
||||||
|
component={"div"}
|
||||||
|
variant="caption"
|
||||||
|
style={{ whiteSpace: "normal" }}
|
||||||
|
>
|
||||||
|
{e.description}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
116
virtweb_frontend/src/widgets/forms/VMNetworksList.tsx
Normal file
116
virtweb_frontend/src/widgets/forms/VMNetworksList.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user