Can specify MAC addresses for domains

This commit is contained in:
Pierre HUBERT 2023-12-19 13:26:56 +01:00
parent 6c56b62833
commit 924c972984
9 changed files with 122 additions and 18 deletions

View File

@ -43,3 +43,6 @@ pub const DISK_SIZE_MIN: usize = 100;
/// Disk size max (MB) /// Disk size max (MB)
pub const DISK_SIZE_MAX: usize = 1000 * 1000 * 2; pub const DISK_SIZE_MAX: usize = 1000 * 1000 * 2;
/// Network mac address default prefix
pub const NET_MAC_ADDR_PREFIX: &str = "52:54:00";

View File

@ -14,6 +14,7 @@ struct StaticConfig {
local_auth_enabled: bool, local_auth_enabled: bool,
oidc_auth_enabled: bool, oidc_auth_enabled: bool,
iso_mimetypes: &'static [&'static str], iso_mimetypes: &'static [&'static str],
net_mac_prefix: &'static str,
constraints: ServerConstraints, constraints: ServerConstraints,
} }
@ -42,6 +43,7 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
local_auth_enabled: *local_auth, local_auth_enabled: *local_auth,
oidc_auth_enabled: !AppConfig::get().disable_oidc, oidc_auth_enabled: !AppConfig::get().disable_oidc,
iso_mimetypes: &constants::ALLOWED_ISO_MIME_TYPES, iso_mimetypes: &constants::ALLOWED_ISO_MIME_TYPES,
net_mac_prefix: constants::NET_MAC_ADDR_PREFIX,
constraints: ServerConstraints { constraints: ServerConstraints {
iso_max_size: constants::ISO_MAX_SIZE, iso_max_size: constants::ISO_MAX_SIZE,

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 = "mac")]
pub struct NetMacAddress {
#[serde(rename(serialize = "@address"))]
pub address: String,
}
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "source")] #[serde(rename = "source")]
pub struct NetIntSourceXML { pub struct NetIntSourceXML {
@ -75,7 +82,7 @@ pub struct NetIntSourceXML {
pub struct DomainNetInterfaceXML { pub struct DomainNetInterfaceXML {
#[serde(rename(serialize = "@type"))] #[serde(rename(serialize = "@type"))]
pub r#type: String, pub r#type: String,
pub mac: NetMacAddress,
pub source: Option<NetIntSourceXML>, pub source: Option<NetIntSourceXML>,
} }

View File

@ -3,10 +3,10 @@ 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, DomainInputXML, DomainMemoryXML, DomainNetInterfaceXML, DomainCPUTopology, DomainCPUXML, DomainInputXML, DomainMemoryXML, DomainNetInterfaceXML,
DomainVCPUXML, DomainXML, FeaturesXML, GraphicsXML, NetIntSourceXML, NetworkDHCPRangeXML, DomainVCPUXML, DomainXML, FeaturesXML, GraphicsXML, NetIntSourceXML, NetMacAddress,
NetworkDHCPXML, NetworkDNSForwarderXML, NetworkDNSXML, NetworkDomainXML, NetworkForwardXML, NetworkDHCPRangeXML, NetworkDHCPXML, NetworkDNSForwarderXML, NetworkDNSXML, NetworkDomainXML,
NetworkIPXML, NetworkXML, OSLoaderXML, OSTypeXML, TPMBackendXML, TPMDeviceXML, VideoModelXML, NetworkForwardXML, NetworkIPXML, NetworkXML, OSLoaderXML, OSTypeXML, TPMBackendXML,
VideoXML, XMLUuid, ACPIXML, OSXML, TPMDeviceXML, VideoModelXML, VideoXML, 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;
@ -65,9 +65,16 @@ pub enum VMArchitecture {
X86_64, X86_64,
} }
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Network {
mac: String,
#[serde(flatten)]
r#type: NetworkType,
}
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum Network { pub enum NetworkType {
UserspaceSLIRPStack, UserspaceSLIRPStack,
DefinedNetwork { network: String }, // TODO : complete network types DefinedNetwork { network: String }, // TODO : complete network types
} }
@ -235,12 +242,14 @@ impl VMInfo {
let mut networks = vec![]; let mut networks = vec![];
for n in self.networks { for n in self.networks {
networks.push(match n { networks.push(match n.r#type {
Network::UserspaceSLIRPStack => DomainNetInterfaceXML { NetworkType::UserspaceSLIRPStack => DomainNetInterfaceXML {
mac: NetMacAddress { address: n.mac },
r#type: "user".to_string(), r#type: "user".to_string(),
source: None, source: None,
}, },
Network::DefinedNetwork { network } => DomainNetInterfaceXML { NetworkType::DefinedNetwork { network } => DomainNetInterfaceXML {
mac: NetMacAddress { address: n.mac },
r#type: "network".to_string(), r#type: "network".to_string(),
source: Some(NetIntSourceXML { network }), source: Some(NetIntSourceXML { network }),
}, },
@ -382,14 +391,21 @@ impl VMInfo {
.devices .devices
.net_interfaces .net_interfaces
.iter() .iter()
.map(|d| match d.r#type.as_str() { .map(|d| {
"user" => Ok(Network::UserspaceSLIRPStack), Ok(Network {
"network" => Ok(Network::DefinedNetwork { mac: d.mac.address.to_string(),
r#type: match d.r#type.as_str() {
"user" => NetworkType::UserspaceSLIRPStack,
"network" => NetworkType::DefinedNetwork {
network: d.source.as_ref().unwrap().network.to_string(), network: d.source.as_ref().unwrap().network.to_string(),
}), },
a => Err(LibVirtStructError::DomainExtraction(format!( a => {
return Err(LibVirtStructError::DomainExtraction(format!(
"Unknown network interface type: {a}! " "Unknown network interface type: {a}! "
))), )));
}
},
})
}) })
.collect::<Result<Vec<_>, _>>()?, .collect::<Result<Vec<_>, _>>()?,

View File

@ -5,6 +5,7 @@ export interface ServerConfig {
local_auth_enabled: boolean; local_auth_enabled: boolean;
oidc_auth_enabled: boolean; oidc_auth_enabled: boolean;
iso_mimetypes: string[]; iso_mimetypes: string[];
net_mac_prefix: string;
constraints: ServerConstraints; constraints: ServerConstraints;
} }

View File

@ -34,10 +34,12 @@ export type VMNetInterface = VMNetUserspaceSLIRPStack | VMNetDefinedNetwork;
export interface VMNetUserspaceSLIRPStack { export interface VMNetUserspaceSLIRPStack {
type: "UserspaceSLIRPStack"; type: "UserspaceSLIRPStack";
mac: string;
} }
export interface VMNetDefinedNetwork { export interface VMNetDefinedNetwork {
type: "DefinedNetwork"; type: "DefinedNetwork";
mac: string;
network: string; network: string;
} }

View File

@ -0,0 +1,11 @@
/**
* Generate a random MAC address
*/
export function randomMacAddress(prefix: string | undefined): string {
let mac = "XX:XX:XX:XX:XX:XX";
mac = prefix + mac.slice(prefix?.length);
return mac.replace(/X/g, () =>
"0123456789abcdef".charAt(Math.floor(Math.random() * 16))
);
}

View File

@ -0,0 +1,46 @@
import { TextInput } from "./TextInput";
export function MACInput(p: {
label: string;
editable: boolean;
value?: string;
onValueChange?: (newVal: string | undefined) => void;
}): React.ReactElement {
const { onValueChange, ...props } = p;
return (
<TextInput
onValueChange={(v) => {
onValueChange?.(sanitizeMacAddress(v));
}}
{...props}
/>
);
}
function sanitizeMacAddress(s: string | undefined): string | undefined {
if (s === "" || s === undefined) return s;
const split = s.split(":");
if (split.length > 6) split.splice(6);
let needAnotherIteration = false;
const res = split
.map((e) => {
if (e === "") return e;
const num = parseInt(e, 16);
if (isNaN(num)) return "0";
let s = num.toString(16).padStart(2, "0");
if (num > 0xff) {
needAnotherIteration = true;
return s.slice(0, 2) + ":" + s.slice(2);
}
return s;
})
.join(":");
return needAnotherIteration ? sanitizeMacAddress(res) : res;
}

View File

@ -14,6 +14,9 @@ import { VMInfo, VMNetInterface } from "../../api/VMApi";
import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider"; import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
import { SelectInput } from "./SelectInput"; import { SelectInput } from "./SelectInput";
import { NetworkInfo } from "../../api/NetworksApi"; import { NetworkInfo } from "../../api/NetworksApi";
import { randomMacAddress } from "../../utils/RandUtils";
import { ServerApi } from "../../api/ServerApi";
import { MACInput } from "./MACInput";
export function VMNetworksList(p: { export function VMNetworksList(p: {
vm: VMInfo; vm: VMInfo;
@ -22,7 +25,10 @@ export function VMNetworksList(p: {
networksList: NetworkInfo[]; networksList: NetworkInfo[];
}): React.ReactElement { }): React.ReactElement {
const addNew = () => { const addNew = () => {
p.vm.networks.push({ type: "UserspaceSLIRPStack" }); p.vm.networks.push({
type: "UserspaceSLIRPStack",
mac: randomMacAddress(ServerApi.Config.net_mac_prefix),
});
p.onChange?.(); p.onChange?.();
}; };
@ -120,6 +126,16 @@ function NetworkInfoWidget(p: {
/> />
</ListItem> </ListItem>
<div style={{ marginLeft: "70px" }}> <div style={{ marginLeft: "70px" }}>
<MACInput
editable={p.editable}
label="MAC Address"
value={p.network.mac}
onValueChange={(v) => {
p.network.mac = v!;
p.onChange?.();
}}
/>
{p.network.type === "DefinedNetwork" && ( {p.network.type === "DefinedNetwork" && (
<SelectInput <SelectInput
editable={p.editable} editable={p.editable}