diff --git a/virtweb_backend/src/actors/libvirt_actor.rs b/virtweb_backend/src/actors/libvirt_actor.rs index 05127cf..954d803 100644 --- a/virtweb_backend/src/actors/libvirt_actor.rs +++ b/virtweb_backend/src/actors/libvirt_actor.rs @@ -1,8 +1,11 @@ use crate::app_config::AppConfig; -use crate::libvirt_lib_structures::{ - DomainState, DomainXML, NetworkFilterXML, NetworkXML, XMLUuid, -}; -use crate::libvirt_rest_structures::*; +use crate::libvirt_lib_structures::domain::*; +use crate::libvirt_lib_structures::network::*; +use crate::libvirt_lib_structures::nwfilter::*; +use crate::libvirt_lib_structures::*; +use crate::libvirt_rest_structures::hypervisor::*; +use crate::libvirt_rest_structures::net::*; +use crate::libvirt_rest_structures::vm::*; use actix::{Actor, Context, Handler, Message}; use image::ImageOutputFormat; use std::io::Cursor; diff --git a/virtweb_backend/src/controllers/network_controller.rs b/virtweb_backend/src/controllers/network_controller.rs index 46d5fc8..b969e56 100644 --- a/virtweb_backend/src/controllers/network_controller.rs +++ b/virtweb_backend/src/controllers/network_controller.rs @@ -1,6 +1,6 @@ use crate::controllers::{HttpResult, LibVirtReq}; use crate::libvirt_lib_structures::XMLUuid; -use crate::libvirt_rest_structures::NetworkInfo; +use crate::libvirt_rest_structures::net::NetworkInfo; use actix_web::{web, HttpResponse}; #[derive(serde::Serialize, serde::Deserialize)] diff --git a/virtweb_backend/src/controllers/nwfilter_controller.rs b/virtweb_backend/src/controllers/nwfilter_controller.rs index 5edc8c3..e8eb53d 100644 --- a/virtweb_backend/src/controllers/nwfilter_controller.rs +++ b/virtweb_backend/src/controllers/nwfilter_controller.rs @@ -1,6 +1,6 @@ use crate::controllers::{HttpResult, LibVirtReq}; use crate::libvirt_lib_structures::XMLUuid; -use crate::libvirt_rest_structures::NetworkFilter; +use crate::libvirt_rest_structures::nw_filter::NetworkFilter; use actix_web::{web, HttpResponse}; #[derive(serde::Serialize, serde::Deserialize)] diff --git a/virtweb_backend/src/controllers/server_controller.rs b/virtweb_backend/src/controllers/server_controller.rs index b61dd2a..ba57a7a 100644 --- a/virtweb_backend/src/controllers/server_controller.rs +++ b/virtweb_backend/src/controllers/server_controller.rs @@ -4,7 +4,7 @@ use crate::constants; use crate::constants::{DISK_NAME_MAX_LEN, DISK_NAME_MIN_LEN, DISK_SIZE_MAX, DISK_SIZE_MIN}; use crate::controllers::{HttpResult, LibVirtReq}; use crate::extractors::local_auth_extractor::LocalAuthEnabled; -use crate::libvirt_rest_structures::HypervisorInfo; +use crate::libvirt_rest_structures::hypervisor::HypervisorInfo; use actix_web::{HttpResponse, Responder}; use sysinfo::{NetworksExt, System, SystemExt}; diff --git a/virtweb_backend/src/controllers/vm_controller.rs b/virtweb_backend/src/controllers/vm_controller.rs index 38e960e..d5c9d70 100644 --- a/virtweb_backend/src/controllers/vm_controller.rs +++ b/virtweb_backend/src/controllers/vm_controller.rs @@ -1,8 +1,9 @@ use crate::actors::vnc_actor::VNCActor; use crate::actors::vnc_tokens_actor::VNCTokensManager; use crate::controllers::{HttpResult, LibVirtReq}; -use crate::libvirt_lib_structures::{DomainState, XMLUuid}; -use crate::libvirt_rest_structures::VMInfo; +use crate::libvirt_lib_structures::domain::DomainState; +use crate::libvirt_lib_structures::XMLUuid; +use crate::libvirt_rest_structures::vm::VMInfo; use actix_web::{web, HttpRequest, HttpResponse}; use actix_web_actors::ws; diff --git a/virtweb_backend/src/libvirt_client.rs b/virtweb_backend/src/libvirt_client.rs index 9b74f75..51f4660 100644 --- a/virtweb_backend/src/libvirt_client.rs +++ b/virtweb_backend/src/libvirt_client.rs @@ -1,9 +1,12 @@ use crate::actors::libvirt_actor; use crate::actors::libvirt_actor::LibVirtActor; -use crate::libvirt_lib_structures::{ - DomainState, DomainXML, NetworkFilterXML, NetworkXML, XMLUuid, -}; -use crate::libvirt_rest_structures::{HypervisorInfo, NetworkInfo, VMInfo}; +use crate::libvirt_lib_structures::domain::{DomainState, DomainXML}; +use crate::libvirt_lib_structures::network::NetworkXML; +use crate::libvirt_lib_structures::nwfilter::NetworkFilterXML; +use crate::libvirt_lib_structures::XMLUuid; +use crate::libvirt_rest_structures::hypervisor::HypervisorInfo; +use crate::libvirt_rest_structures::net::NetworkInfo; +use crate::libvirt_rest_structures::vm::VMInfo; use actix::Addr; #[derive(Clone)] diff --git a/virtweb_backend/src/libvirt_lib_structures.rs b/virtweb_backend/src/libvirt_lib_structures.rs deleted file mode 100644 index 51d25fe..0000000 --- a/virtweb_backend/src/libvirt_lib_structures.rs +++ /dev/null @@ -1,864 +0,0 @@ -use std::fmt::Display; -use std::net::{IpAddr, Ipv4Addr}; - -#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)] -pub struct XMLUuid(pub uuid::Uuid); - -impl XMLUuid { - pub fn parse_from_str(s: &str) -> anyhow::Result { - Ok(Self(uuid::Uuid::parse_str(s)?)) - } - - pub fn new_random() -> Self { - Self(uuid::Uuid::new_v4()) - } - pub fn as_string(&self) -> String { - self.0.to_string() - } - - pub fn is_valid(&self) -> bool { - log::debug!("UUID version ({}): {}", self.0, self.0.get_version_num()); - self.0.get_version_num() == 4 - } -} - -/// OS information -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename = "os")] -pub struct OSXML { - #[serde(rename(serialize = "@firmware"), default)] - pub firmware: String, - pub r#type: OSTypeXML, - pub loader: Option, -} - -/// OS Type information -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename = "os")] -pub struct OSTypeXML { - #[serde(rename(serialize = "@arch"))] - pub arch: String, - #[serde(rename(serialize = "@machine"))] - pub machine: String, - #[serde(rename = "$value")] - pub body: String, -} - -/// OS Loader information -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename = "loader")] -pub struct OSLoaderXML { - #[serde(rename(serialize = "@secure"))] - pub secure: String, -} - -/// Hypervisor features -#[derive(serde::Serialize, serde::Deserialize, Default)] -#[serde(rename = "features")] -pub struct FeaturesXML { - pub acpi: ACPIXML, -} - -/// ACPI feature -#[derive(serde::Serialize, serde::Deserialize, Default)] -#[serde(rename = "acpi")] -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)] -#[serde(rename = "source")] -pub struct NetIntSourceXML { - #[serde(rename(serialize = "@network"))] - pub network: String, -} - -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename = "model")] -pub struct NetIntModelXML { - #[serde(rename(serialize = "@type"))] - pub r#type: String, -} - -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename = "interface")] -pub struct DomainNetInterfaceXML { - #[serde(rename(serialize = "@type"))] - pub r#type: String, - pub mac: NetMacAddress, - #[serde(skip_serializing_if = "Option::is_none")] - pub source: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub model: Option, -} - -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename = "input")] -pub struct DomainInputXML { - #[serde(rename(serialize = "@type"))] - pub r#type: String, -} - -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename = "backend")] -pub struct TPMBackendXML { - #[serde(rename(serialize = "@type"))] - pub r#type: String, - - #[serde(rename(serialize = "@version"))] - pub r#version: String, -} - -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename = "tpm")] -pub struct TPMDeviceXML { - #[serde(rename(serialize = "@model"))] - pub model: String, - pub backend: TPMBackendXML, -} - -/// Devices information -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename = "devices")] -pub struct DevicesXML { - /// Graphics (used for VNC) - #[serde(skip_serializing_if = "Option::is_none")] - pub graphics: Option, - - /// Graphics (used for VNC) - #[serde(skip_serializing_if = "Option::is_none")] - pub video: Option, - - /// 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, - - /// Input devices - #[serde(default, rename = "input", skip_serializing_if = "Vec::is_empty")] - pub inputs: Vec, - - /// TPM device - #[serde(skip_serializing_if = "Option::is_none")] - pub tpm: Option, -} - -/// Graphics information -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename = "graphics")] -pub struct GraphicsXML { - #[serde(rename(serialize = "@type"))] - pub r#type: String, - #[serde(rename(serialize = "@socket"))] - pub socket: String, -} - -/// Video device information -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename = "video")] -pub struct VideoXML { - pub model: VideoModelXML, -} - -/// Video model device information -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename = "model")] -pub struct VideoModelXML { - #[serde(rename(serialize = "@type"))] - pub r#type: String, -} - -/// Disk information -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename = "disk")] -pub struct DiskXML { - #[serde(rename(serialize = "@type"))] - pub r#type: String, - #[serde(rename(serialize = "@device"))] - pub r#device: String, - - pub driver: DiskDriverXML, - pub source: DiskSourceXML, - pub target: DiskTargetXML, - #[serde(skip_serializing_if = "Option::is_none")] - pub readonly: Option, - pub boot: DiskBootXML, - #[serde(skip_serializing_if = "Option::is_none")] - pub address: Option, -} - -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename = "driver")] -pub struct DiskDriverXML { - #[serde(rename(serialize = "@name"))] - pub name: String, - #[serde(rename(serialize = "@type"))] - pub r#type: String, - #[serde(default, rename(serialize = "@cache"))] - pub r#cache: String, -} - -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename = "source")] -pub struct DiskSourceXML { - #[serde(rename(serialize = "@file"))] - pub file: String, -} - -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename = "target")] -pub struct DiskTargetXML { - #[serde(rename(serialize = "@dev"))] - pub dev: String, - #[serde(rename(serialize = "@bus"))] - pub bus: String, -} - -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename = "readonly")] -pub struct DiskReadOnlyXML {} - -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename = "boot")] -pub struct DiskBootXML { - #[serde(rename(serialize = "@order"))] - pub order: String, -} - -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename = "address")] -pub struct DiskAddressXML { - #[serde(rename(serialize = "@type"))] - pub r#type: String, - #[serde( - default, - skip_serializing_if = "Option::is_none", - rename(serialize = "@controller") - )] - pub r#controller: Option, - #[serde(rename(serialize = "@bus"))] - pub r#bus: String, - #[serde( - default, - skip_serializing_if = "Option::is_none", - rename(serialize = "@target") - )] - pub r#target: Option, - #[serde( - default, - skip_serializing_if = "Option::is_none", - rename(serialize = "@unit") - )] - pub r#unit: Option, -} - -/// Domain RAM information -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename = "memory")] -pub struct DomainMemoryXML { - #[serde(rename(serialize = "@unit"))] - pub unit: String, - - #[serde(rename = "$value")] - pub memory: usize, -} - -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename = "topology")] -pub struct DomainCPUTopology { - #[serde(rename(serialize = "@sockets"))] - pub sockets: usize, - #[serde(rename(serialize = "@cores"))] - pub cores: usize, - #[serde(rename(serialize = "@threads"))] - pub threads: usize, -} - -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename = "cpu")] -pub struct DomainVCPUXML { - #[serde(rename = "$value")] - pub body: usize, -} - -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename = "cpu")] -pub struct DomainCPUXML { - #[serde(rename(serialize = "@mode"))] - pub mode: String, - pub topology: Option, -} - -/// Domain information, see https://libvirt.org/formatdomain.html -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename = "domain")] -pub struct DomainXML { - /// Domain type (kvm) - #[serde(rename(serialize = "@type"))] - pub r#type: String, - - pub name: String, - pub uuid: Option, - pub genid: Option, - pub title: Option, - pub description: Option, - pub os: OSXML, - #[serde(default)] - pub features: FeaturesXML, - pub devices: DevicesXML, - - /// The maximum allocation of memory for the guest at boot time - pub memory: DomainMemoryXML, - - /// Number of vCPU - pub vcpu: DomainVCPUXML, - - /// CPU information - pub cpu: DomainCPUXML, - - pub on_poweroff: String, - pub on_reboot: String, - pub on_crash: String, -} - -impl DomainXML { - /// Turn this domain into its XML definition - pub fn into_xml(mut self) -> anyhow::Result { - // A issue with the disks & network interface definition serialization needs them to be serialized aside - let mut devices_xml = Vec::with_capacity(self.devices.disks.len()); - for disk in self.devices.disks { - let disk_xml = serde_xml_rs::to_string(&disk)?; - let start_offset = disk_xml.find("", &format!("{disks_xml}"), 1); - Ok(xml) - } -} - -/// Domain state -#[derive(serde::Serialize, Debug, Copy, Clone)] -pub enum DomainState { - NoState, - Running, - Blocked, - Paused, - Shutdown, - Shutoff, - Crashed, - PowerManagementSuspended, - Other, -} - -/// Network forward information -#[derive(serde::Serialize, serde::Deserialize, Debug)] -#[serde(rename = "forward")] -pub struct NetworkForwardXML { - #[serde(rename(serialize = "@mode"))] - pub mode: String, - #[serde( - default, - rename(serialize = "@dev"), - skip_serializing_if = "String::is_empty" - )] - pub dev: String, -} - -/// Network bridge information -#[derive(serde::Serialize, serde::Deserialize, Debug)] -#[serde(rename = "bridge")] -pub struct NetworkBridgeXML { - #[serde(rename(serialize = "@name"))] - pub name: String, -} - -/// Network DNS information -#[derive(serde::Serialize, serde::Deserialize, Debug)] -#[serde(rename = "dns")] -pub struct NetworkDNSXML { - pub forwarder: NetworkDNSForwarderXML, -} - -/// Network DNS information -#[derive(serde::Serialize, serde::Deserialize, Debug)] -#[serde(rename = "forwarder")] -pub struct NetworkDNSForwarderXML { - /// Address of the DNS server - #[serde(rename(serialize = "@addr"))] - pub addr: Ipv4Addr, -} - -/// Network DNS information -#[derive(serde::Serialize, serde::Deserialize, Debug)] -#[serde(rename = "domain")] -pub struct NetworkDomainXML { - #[serde(rename(serialize = "@name"))] - pub name: String, -} - -fn invalid_prefix() -> u32 { - u32::MAX -} - -fn invalid_ip() -> IpAddr { - IpAddr::V4(Ipv4Addr::BROADCAST) -} - -/// Network ip information -#[derive(serde::Serialize, serde::Deserialize, Debug)] -#[serde(rename = "ip")] -pub struct NetworkIPXML { - #[serde(default, rename(serialize = "@family"))] - pub family: String, - #[serde(rename(serialize = "@address"))] - pub address: IpAddr, - /// Network Prefix - #[serde(rename(serialize = "@prefix"), default = "invalid_prefix")] - pub prefix: u32, - /// Network Netmask. This field is never serialized, but because we can't know if LibVirt will - /// provide us netmask or prefix, we need to handle both of these fields - #[serde( - rename(serialize = "@netmask"), - default = "invalid_ip", - skip_serializing - )] - pub netmask: IpAddr, - pub dhcp: Option, -} - -impl NetworkIPXML { - pub fn into_xml(mut self) -> anyhow::Result { - let mut hosts_xml = vec![]; - - if let Some(dhcp) = &mut self.dhcp { - for host in &dhcp.hosts { - let mut host_xml = serde_xml_rs::to_string(&host)?; - - // In case of IPv6, mac address should not be specified - host_xml = host_xml.replace("mac=\"\"", ""); - - // strip xml tag - let start_offset = host_xml.find("", &format!("{hosts_xml}")); - Ok(res) - } -} - -#[derive(serde::Serialize, serde::Deserialize, Debug)] -#[serde(rename = "dhcp")] -pub struct NetworkDHCPXML { - pub range: NetworkDHCPRangeXML, - #[serde(default, rename = "host", skip_serializing_if = "Vec::is_empty")] - pub hosts: Vec, -} - -#[derive(serde::Serialize, serde::Deserialize, Debug)] -#[serde(rename = "host")] -pub struct NetworkDHCPHostXML { - #[serde(rename(serialize = "@mac"), default)] - pub mac: String, - #[serde(rename(serialize = "@name"))] - pub name: String, - #[serde(rename(serialize = "@ip"))] - pub ip: IpAddr, -} - -#[derive(serde::Serialize, serde::Deserialize, Debug)] -#[serde(rename = "dhcp")] -pub struct NetworkDHCPRangeXML { - #[serde(rename(serialize = "@start"))] - pub start: IpAddr, - #[serde(rename(serialize = "@end"))] - pub end: IpAddr, -} - -/// Network information, see https://libvirt.org/formatnetwork.html -#[derive(serde::Serialize, serde::Deserialize, Debug)] -#[serde(rename = "network")] -pub struct NetworkXML { - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub uuid: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub forward: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub bridge: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub dns: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub domain: Option, - #[serde(default, rename = "ip")] - pub ips: Vec, -} - -impl NetworkXML { - pub fn into_xml(mut self) -> anyhow::Result { - // A issue with the IPs definition serialization needs them to be serialized aside - let mut ips_xml = Vec::with_capacity(self.ips.len()); - for ip in self.ips { - log::debug!("Serialize {ip:?}"); - let ip_xml = ip.into_xml()?; - // strip xml tag - let start_offset = ip_xml.find("", &format!("{ips_xml}"), 1); - Ok(network_xml) - } -} - -#[derive(serde::Serialize, serde::Deserialize, Debug)] -#[serde(rename = "filterref")] -pub struct NetworkFilterRefXML { - #[serde(rename(serialize = "@filter"))] - pub filter: String, -} - -#[derive(serde::Serialize, serde::Deserialize, Debug)] -#[serde(rename = "all")] -pub struct NetworkFilterRuleProtocolAll {} - -#[derive(serde::Serialize, serde::Deserialize, Debug)] -#[serde(rename = "mac")] -pub struct NetworkFilterRuleProtocolMac { - #[serde( - rename(serialize = "@srcmacaddr"), - skip_serializing_if = "Option::is_none" - )] - srcmacaddr: Option, - #[serde( - rename(serialize = "@srcmacmask"), - skip_serializing_if = "Option::is_none" - )] - srcmacmask: Option, - #[serde( - rename(serialize = "@dstmacaddr"), - skip_serializing_if = "Option::is_none" - )] - dstmacaddr: Option, - #[serde( - rename(serialize = "@dstmacmask"), - skip_serializing_if = "Option::is_none" - )] - dstmacmask: Option, - #[serde( - rename(serialize = "@comment"), - skip_serializing_if = "Option::is_none" - )] - comment: Option, -} - -#[derive(serde::Serialize, serde::Deserialize, Debug)] -#[serde(rename = "arp")] -pub struct NetworkFilterRuleProtocolArp { - #[serde( - rename(serialize = "@srcmacaddr"), - skip_serializing_if = "Option::is_none" - )] - srcmacaddr: Option, - #[serde( - rename(serialize = "@srcmacmask"), - skip_serializing_if = "Option::is_none" - )] - srcmacmask: Option, - #[serde( - rename(serialize = "@dstmacaddr"), - skip_serializing_if = "Option::is_none" - )] - dstmacaddr: Option, - #[serde( - rename(serialize = "@dstmacmask"), - skip_serializing_if = "Option::is_none" - )] - dstmacmask: Option, - #[serde( - rename(serialize = "@arpsrcipaddr"), - skip_serializing_if = "Option::is_none" - )] - arpsrcipaddr: Option, - #[serde( - rename(serialize = "@arpsrcipmask"), - skip_serializing_if = "Option::is_none" - )] - arpsrcipmask: Option, - #[serde( - rename(serialize = "@arpdstipaddr"), - skip_serializing_if = "Option::is_none" - )] - arpdstipaddr: Option, - #[serde( - rename(serialize = "@arpdstipmask"), - skip_serializing_if = "Option::is_none" - )] - arpdstipmask: Option, - - #[serde( - rename(serialize = "@comment"), - skip_serializing_if = "Option::is_none" - )] - comment: Option, -} - -#[derive(serde::Serialize, serde::Deserialize, Debug)] -#[serde(rename = "ipvx")] -pub struct NetworkFilterRuleProtocolIpvx { - #[serde( - rename(serialize = "@srcmacaddr"), - skip_serializing_if = "Option::is_none" - )] - srcmacaddr: Option, - #[serde( - rename(serialize = "@srcmacmask"), - skip_serializing_if = "Option::is_none" - )] - srcmacmask: Option, - #[serde( - rename(serialize = "@dstmacaddr"), - skip_serializing_if = "Option::is_none" - )] - dstmacaddr: Option, - #[serde( - rename(serialize = "@dstmacmask"), - skip_serializing_if = "Option::is_none" - )] - dstmacmask: Option, - #[serde( - rename(serialize = "@srcipaddr"), - skip_serializing_if = "Option::is_none" - )] - srcipaddr: Option, - #[serde( - rename(serialize = "@srcipmask"), - skip_serializing_if = "Option::is_none" - )] - srcipmask: Option, - #[serde( - rename(serialize = "@dstipaddr"), - skip_serializing_if = "Option::is_none" - )] - dstipaddr: Option, - #[serde( - rename(serialize = "@dstipmask"), - skip_serializing_if = "Option::is_none" - )] - dstipmask: Option, - - #[serde( - rename(serialize = "@comment"), - skip_serializing_if = "Option::is_none" - )] - comment: Option, -} - -#[derive(serde::Serialize, serde::Deserialize, Debug)] -#[serde(rename = "layer4")] -pub struct NetworkFilterRuleProtocolLayer4 { - #[serde( - rename(serialize = "@srcmacaddr"), - skip_serializing_if = "Option::is_none" - )] - srcmacaddr: Option, - #[serde( - rename(serialize = "@srcipaddr"), - skip_serializing_if = "Option::is_none" - )] - srcipaddr: Option, - #[serde( - rename(serialize = "@srcipmask"), - skip_serializing_if = "Option::is_none" - )] - srcipmask: Option, - #[serde( - rename(serialize = "@dstipaddr"), - skip_serializing_if = "Option::is_none" - )] - dstipaddr: Option, - #[serde( - rename(serialize = "@dstipmask"), - skip_serializing_if = "Option::is_none" - )] - dstipmask: Option, - /// Start of range of source IP address - #[serde( - rename(serialize = "@srcipfrom"), - skip_serializing_if = "Option::is_none" - )] - srcipfrom: Option, - /// End of range of source IP address - #[serde( - rename(serialize = "@srcipto"), - skip_serializing_if = "Option::is_none" - )] - srcipto: Option, - /// Start of range of destination IP address - #[serde( - rename(serialize = "@dstipfrom"), - skip_serializing_if = "Option::is_none" - )] - dstipfrom: Option, - /// End of range of destination IP address - #[serde( - rename(serialize = "@dstipto"), - skip_serializing_if = "Option::is_none" - )] - dstipto: Option, - #[serde( - rename(serialize = "@srcportstart"), - skip_serializing_if = "Option::is_none" - )] - srcportstart: Option, - #[serde( - rename(serialize = "@srcportend"), - skip_serializing_if = "Option::is_none" - )] - srcportend: Option, - #[serde( - rename(serialize = "@dstportstart"), - skip_serializing_if = "Option::is_none" - )] - dstportstart: Option, - #[serde( - rename(serialize = "@dstportend"), - skip_serializing_if = "Option::is_none" - )] - dstportend: Option, - #[serde(rename(serialize = "@state"), skip_serializing_if = "Option::is_none")] - state: Option, - - #[serde( - rename(serialize = "@comment"), - skip_serializing_if = "Option::is_none" - )] - comment: Option, -} - -#[derive(serde::Serialize, serde::Deserialize, Debug)] -#[serde(rename = "rule")] -pub struct NetworkFilterRuleXML { - #[serde(rename(serialize = "@action"))] - pub action: String, - #[serde(rename(serialize = "@direction"))] - pub direction: String, - #[serde(rename(serialize = "@priority"))] - pub priority: Option, - - /// Match all protocols - #[serde(skip_serializing_if = "Option::is_none")] - pub all: Option, - - /// Match mac protocol - #[serde(default, rename = "mac", skip_serializing_if = "Vec::is_empty")] - pub mac_rules: Vec, - - /// Match arp protocol - #[serde(default, rename = "arp", skip_serializing_if = "Vec::is_empty")] - pub arp_rules: Vec, - - /// Match IPv4 protocol - #[serde(default, rename = "ip", skip_serializing_if = "Vec::is_empty")] - pub ipv4_rules: Vec, - - /// Match IPv6 protocol - #[serde(default, rename = "ipv6", skip_serializing_if = "Vec::is_empty")] - pub ipv6_rules: Vec, - - /// Match TCP protocol - #[serde(default, rename = "tcp", skip_serializing_if = "Vec::is_empty")] - pub tcp_rules: Vec, - - /// Match UDP protocol - #[serde(default, rename = "udp", skip_serializing_if = "Vec::is_empty")] - pub udp_rules: Vec, - - /// Match SCTP protocol - #[serde(default, rename = "sctp", skip_serializing_if = "Vec::is_empty")] - pub sctp_rules: Vec, -} - -#[derive(serde::Serialize, serde::Deserialize, Debug)] -#[serde(rename = "filter")] -pub struct NetworkFilterXML { - #[serde(rename(serialize = "@name"))] - pub name: String, - #[serde(rename(serialize = "@chain"), default)] - pub chain: String, - #[serde( - skip_serializing_if = "Option::is_none", - rename(serialize = "@priority"), - default - )] - pub priority: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub uuid: Option, - #[serde(default, rename = "filterref", skip_serializing_if = "Vec::is_empty")] - pub filterrefs: Vec, - #[serde(default, rename = "rule", skip_serializing_if = "Vec::is_empty")] - pub rules: Vec, -} - -impl NetworkFilterXML { - pub fn parse_xml(xml: D) -> anyhow::Result { - let xml = xml.to_string(); - - // We need to put all filter refs at the same location - let mut filter_refs = Vec::new(); - let xml = lazy_regex::regex_replace_all!(r#""#, &xml, |r: &str| { - filter_refs.push(r.to_string()); - - if r.contains('\n') { - log::warn!("A filterref contain a new line. This is a symptom of a new unsupported child attribute of object!"); - } - - "" - }); - - let filter_refs = filter_refs.join("\n"); - let xml = xml.replace("", &format!("{filter_refs}")); - log::debug!("Effective NW filter rule parsed: {xml}"); - - Ok(serde_xml_rs::from_str(&xml)?) - } -} diff --git a/virtweb_backend/src/libvirt_lib_structures/domain.rs b/virtweb_backend/src/libvirt_lib_structures/domain.rs new file mode 100644 index 0000000..6f3dde2 --- /dev/null +++ b/virtweb_backend/src/libvirt_lib_structures/domain.rs @@ -0,0 +1,354 @@ +use crate::libvirt_lib_structures::XMLUuid; + +/// OS information +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "os")] +pub struct OSXML { + #[serde(rename(serialize = "@firmware"), default)] + pub firmware: String, + pub r#type: OSTypeXML, + pub loader: Option, +} + +/// OS Type information +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "os")] +pub struct OSTypeXML { + #[serde(rename(serialize = "@arch"))] + pub arch: String, + #[serde(rename(serialize = "@machine"))] + pub machine: String, + #[serde(rename = "$value")] + pub body: String, +} + +/// OS Loader information +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "loader")] +pub struct OSLoaderXML { + #[serde(rename(serialize = "@secure"))] + pub secure: String, +} + +/// Hypervisor features +#[derive(serde::Serialize, serde::Deserialize, Default)] +#[serde(rename = "features")] +pub struct FeaturesXML { + pub acpi: ACPIXML, +} + +/// ACPI feature +#[derive(serde::Serialize, serde::Deserialize, Default)] +#[serde(rename = "acpi")] +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)] +#[serde(rename = "source")] +pub struct NetIntSourceXML { + #[serde(rename(serialize = "@network"))] + pub network: String, +} + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "model")] +pub struct NetIntModelXML { + #[serde(rename(serialize = "@type"))] + pub r#type: String, +} + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "interface")] +pub struct DomainNetInterfaceXML { + #[serde(rename(serialize = "@type"))] + pub r#type: String, + pub mac: NetMacAddress, + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub model: Option, +} + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "input")] +pub struct DomainInputXML { + #[serde(rename(serialize = "@type"))] + pub r#type: String, +} + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "backend")] +pub struct TPMBackendXML { + #[serde(rename(serialize = "@type"))] + pub r#type: String, + + #[serde(rename(serialize = "@version"))] + pub r#version: String, +} + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "tpm")] +pub struct TPMDeviceXML { + #[serde(rename(serialize = "@model"))] + pub model: String, + pub backend: TPMBackendXML, +} + +/// Devices information +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "devices")] +pub struct DevicesXML { + /// Graphics (used for VNC) + #[serde(skip_serializing_if = "Option::is_none")] + pub graphics: Option, + + /// Graphics (used for VNC) + #[serde(skip_serializing_if = "Option::is_none")] + pub video: Option, + + /// 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, + + /// Input devices + #[serde(default, rename = "input", skip_serializing_if = "Vec::is_empty")] + pub inputs: Vec, + + /// TPM device + #[serde(skip_serializing_if = "Option::is_none")] + pub tpm: Option, +} + +/// Graphics information +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "graphics")] +pub struct GraphicsXML { + #[serde(rename(serialize = "@type"))] + pub r#type: String, + #[serde(rename(serialize = "@socket"))] + pub socket: String, +} + +/// Video device information +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "video")] +pub struct VideoXML { + pub model: VideoModelXML, +} + +/// Video model device information +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "model")] +pub struct VideoModelXML { + #[serde(rename(serialize = "@type"))] + pub r#type: String, +} + +/// Disk information +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "disk")] +pub struct DiskXML { + #[serde(rename(serialize = "@type"))] + pub r#type: String, + #[serde(rename(serialize = "@device"))] + pub r#device: String, + + pub driver: DiskDriverXML, + pub source: DiskSourceXML, + pub target: DiskTargetXML, + #[serde(skip_serializing_if = "Option::is_none")] + pub readonly: Option, + pub boot: DiskBootXML, + #[serde(skip_serializing_if = "Option::is_none")] + pub address: Option, +} + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "driver")] +pub struct DiskDriverXML { + #[serde(rename(serialize = "@name"))] + pub name: String, + #[serde(rename(serialize = "@type"))] + pub r#type: String, + #[serde(default, rename(serialize = "@cache"))] + pub r#cache: String, +} + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "source")] +pub struct DiskSourceXML { + #[serde(rename(serialize = "@file"))] + pub file: String, +} + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "target")] +pub struct DiskTargetXML { + #[serde(rename(serialize = "@dev"))] + pub dev: String, + #[serde(rename(serialize = "@bus"))] + pub bus: String, +} + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "readonly")] +pub struct DiskReadOnlyXML {} + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "boot")] +pub struct DiskBootXML { + #[serde(rename(serialize = "@order"))] + pub order: String, +} + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "address")] +pub struct DiskAddressXML { + #[serde(rename(serialize = "@type"))] + pub r#type: String, + #[serde( + default, + skip_serializing_if = "Option::is_none", + rename(serialize = "@controller") + )] + pub r#controller: Option, + #[serde(rename(serialize = "@bus"))] + pub r#bus: String, + #[serde( + default, + skip_serializing_if = "Option::is_none", + rename(serialize = "@target") + )] + pub r#target: Option, + #[serde( + default, + skip_serializing_if = "Option::is_none", + rename(serialize = "@unit") + )] + pub r#unit: Option, +} + +/// Domain RAM information +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "memory")] +pub struct DomainMemoryXML { + #[serde(rename(serialize = "@unit"))] + pub unit: String, + + #[serde(rename = "$value")] + pub memory: usize, +} + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "topology")] +pub struct DomainCPUTopology { + #[serde(rename(serialize = "@sockets"))] + pub sockets: usize, + #[serde(rename(serialize = "@cores"))] + pub cores: usize, + #[serde(rename(serialize = "@threads"))] + pub threads: usize, +} + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "cpu")] +pub struct DomainVCPUXML { + #[serde(rename = "$value")] + pub body: usize, +} + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "cpu")] +pub struct DomainCPUXML { + #[serde(rename(serialize = "@mode"))] + pub mode: String, + pub topology: Option, +} + +/// Domain information, see https://libvirt.org/formatdomain.html +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename = "domain")] +pub struct DomainXML { + /// Domain type (kvm) + #[serde(rename(serialize = "@type"))] + pub r#type: String, + + pub name: String, + pub uuid: Option, + pub genid: Option, + pub title: Option, + pub description: Option, + pub os: OSXML, + #[serde(default)] + pub features: FeaturesXML, + pub devices: DevicesXML, + + /// The maximum allocation of memory for the guest at boot time + pub memory: DomainMemoryXML, + + /// Number of vCPU + pub vcpu: DomainVCPUXML, + + /// CPU information + pub cpu: DomainCPUXML, + + pub on_poweroff: String, + pub on_reboot: String, + pub on_crash: String, +} + +impl DomainXML { + /// Turn this domain into its XML definition + pub fn into_xml(mut self) -> anyhow::Result { + // A issue with the disks & network interface definition serialization needs them to be serialized aside + let mut devices_xml = Vec::with_capacity(self.devices.disks.len()); + for disk in self.devices.disks { + let disk_xml = serde_xml_rs::to_string(&disk)?; + let start_offset = disk_xml.find("", &format!("{disks_xml}"), 1); + Ok(xml) + } +} + +/// Domain state +#[derive(serde::Serialize, Debug, Copy, Clone)] +pub enum DomainState { + NoState, + Running, + Blocked, + Paused, + Shutdown, + Shutoff, + Crashed, + PowerManagementSuspended, + Other, +} diff --git a/virtweb_backend/src/libvirt_lib_structures/mod.rs b/virtweb_backend/src/libvirt_lib_structures/mod.rs new file mode 100644 index 0000000..59adfa6 --- /dev/null +++ b/virtweb_backend/src/libvirt_lib_structures/mod.rs @@ -0,0 +1,24 @@ +#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)] +pub struct XMLUuid(pub uuid::Uuid); + +impl XMLUuid { + pub fn parse_from_str(s: &str) -> anyhow::Result { + Ok(Self(uuid::Uuid::parse_str(s)?)) + } + + pub fn new_random() -> Self { + Self(uuid::Uuid::new_v4()) + } + pub fn as_string(&self) -> String { + self.0.to_string() + } + + pub fn is_valid(&self) -> bool { + log::debug!("UUID version ({}): {}", self.0, self.0.get_version_num()); + self.0.get_version_num() == 4 + } +} + +pub mod domain; +pub mod network; +pub mod nwfilter; diff --git a/virtweb_backend/src/libvirt_lib_structures/network.rs b/virtweb_backend/src/libvirt_lib_structures/network.rs new file mode 100644 index 0000000..71fc82d --- /dev/null +++ b/virtweb_backend/src/libvirt_lib_structures/network.rs @@ -0,0 +1,177 @@ +use crate::libvirt_lib_structures::XMLUuid; +use std::net::{IpAddr, Ipv4Addr}; + +/// Network forward information +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename = "forward")] +pub struct NetworkForwardXML { + #[serde(rename(serialize = "@mode"))] + pub mode: String, + #[serde( + default, + rename(serialize = "@dev"), + skip_serializing_if = "String::is_empty" + )] + pub dev: String, +} + +/// Network bridge information +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename = "bridge")] +pub struct NetworkBridgeXML { + #[serde(rename(serialize = "@name"))] + pub name: String, +} + +/// Network DNS information +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename = "dns")] +pub struct NetworkDNSXML { + pub forwarder: NetworkDNSForwarderXML, +} + +/// Network DNS information +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename = "forwarder")] +pub struct NetworkDNSForwarderXML { + /// Address of the DNS server + #[serde(rename(serialize = "@addr"))] + pub addr: Ipv4Addr, +} + +/// Network DNS information +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename = "domain")] +pub struct NetworkDomainXML { + #[serde(rename(serialize = "@name"))] + pub name: String, +} + +fn invalid_prefix() -> u32 { + u32::MAX +} + +fn invalid_ip() -> IpAddr { + IpAddr::V4(Ipv4Addr::BROADCAST) +} + +/// Network ip information +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename = "ip")] +pub struct NetworkIPXML { + #[serde(default, rename(serialize = "@family"))] + pub family: String, + #[serde(rename(serialize = "@address"))] + pub address: IpAddr, + /// Network Prefix + #[serde(rename(serialize = "@prefix"), default = "invalid_prefix")] + pub prefix: u32, + /// Network Netmask. This field is never serialized, but because we can't know if LibVirt will + /// provide us netmask or prefix, we need to handle both of these fields + #[serde( + rename(serialize = "@netmask"), + default = "invalid_ip", + skip_serializing + )] + pub netmask: IpAddr, + pub dhcp: Option, +} + +impl NetworkIPXML { + pub fn into_xml(mut self) -> anyhow::Result { + let mut hosts_xml = vec![]; + + if let Some(dhcp) = &mut self.dhcp { + for host in &dhcp.hosts { + let mut host_xml = serde_xml_rs::to_string(&host)?; + + // In case of IPv6, mac address should not be specified + host_xml = host_xml.replace("mac=\"\"", ""); + + // strip xml tag + let start_offset = host_xml.find("", &format!("{hosts_xml}")); + Ok(res) + } +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename = "dhcp")] +pub struct NetworkDHCPXML { + pub range: NetworkDHCPRangeXML, + #[serde(default, rename = "host", skip_serializing_if = "Vec::is_empty")] + pub hosts: Vec, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename = "host")] +pub struct NetworkDHCPHostXML { + #[serde(rename(serialize = "@mac"), default)] + pub mac: String, + #[serde(rename(serialize = "@name"))] + pub name: String, + #[serde(rename(serialize = "@ip"))] + pub ip: IpAddr, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename = "dhcp")] +pub struct NetworkDHCPRangeXML { + #[serde(rename(serialize = "@start"))] + pub start: IpAddr, + #[serde(rename(serialize = "@end"))] + pub end: IpAddr, +} + +/// Network information, see https://libvirt.org/formatnetwork.html +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename = "network")] +pub struct NetworkXML { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub uuid: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub forward: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub bridge: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub dns: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub domain: Option, + #[serde(default, rename = "ip")] + pub ips: Vec, +} + +impl NetworkXML { + pub fn into_xml(mut self) -> anyhow::Result { + // A issue with the IPs definition serialization needs them to be serialized aside + let mut ips_xml = Vec::with_capacity(self.ips.len()); + for ip in self.ips { + log::debug!("Serialize {ip:?}"); + let ip_xml = ip.into_xml()?; + // strip xml tag + let start_offset = ip_xml.find("", &format!("{ips_xml}"), 1); + Ok(network_xml) + } +} diff --git a/virtweb_backend/src/libvirt_lib_structures/nwfilter.rs b/virtweb_backend/src/libvirt_lib_structures/nwfilter.rs new file mode 100644 index 0000000..059c5f0 --- /dev/null +++ b/virtweb_backend/src/libvirt_lib_structures/nwfilter.rs @@ -0,0 +1,316 @@ +use crate::libvirt_lib_structures::XMLUuid; +use std::fmt::Display; +use std::net::IpAddr; + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename = "filterref")] +pub struct NetworkFilterRefXML { + #[serde(rename(serialize = "@filter"))] + pub filter: String, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename = "all")] +pub struct NetworkFilterRuleProtocolAll {} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename = "mac")] +pub struct NetworkFilterRuleProtocolMac { + #[serde( + rename(serialize = "@srcmacaddr"), + skip_serializing_if = "Option::is_none" + )] + srcmacaddr: Option, + #[serde( + rename(serialize = "@srcmacmask"), + skip_serializing_if = "Option::is_none" + )] + srcmacmask: Option, + #[serde( + rename(serialize = "@dstmacaddr"), + skip_serializing_if = "Option::is_none" + )] + dstmacaddr: Option, + #[serde( + rename(serialize = "@dstmacmask"), + skip_serializing_if = "Option::is_none" + )] + dstmacmask: Option, + #[serde( + rename(serialize = "@comment"), + skip_serializing_if = "Option::is_none" + )] + comment: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename = "arp")] +pub struct NetworkFilterRuleProtocolArp { + #[serde( + rename(serialize = "@srcmacaddr"), + skip_serializing_if = "Option::is_none" + )] + srcmacaddr: Option, + #[serde( + rename(serialize = "@srcmacmask"), + skip_serializing_if = "Option::is_none" + )] + srcmacmask: Option, + #[serde( + rename(serialize = "@dstmacaddr"), + skip_serializing_if = "Option::is_none" + )] + dstmacaddr: Option, + #[serde( + rename(serialize = "@dstmacmask"), + skip_serializing_if = "Option::is_none" + )] + dstmacmask: Option, + #[serde( + rename(serialize = "@arpsrcipaddr"), + skip_serializing_if = "Option::is_none" + )] + arpsrcipaddr: Option, + #[serde( + rename(serialize = "@arpsrcipmask"), + skip_serializing_if = "Option::is_none" + )] + arpsrcipmask: Option, + #[serde( + rename(serialize = "@arpdstipaddr"), + skip_serializing_if = "Option::is_none" + )] + arpdstipaddr: Option, + #[serde( + rename(serialize = "@arpdstipmask"), + skip_serializing_if = "Option::is_none" + )] + arpdstipmask: Option, + + #[serde( + rename(serialize = "@comment"), + skip_serializing_if = "Option::is_none" + )] + comment: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename = "ipvx")] +pub struct NetworkFilterRuleProtocolIpvx { + #[serde( + rename(serialize = "@srcmacaddr"), + skip_serializing_if = "Option::is_none" + )] + srcmacaddr: Option, + #[serde( + rename(serialize = "@srcmacmask"), + skip_serializing_if = "Option::is_none" + )] + srcmacmask: Option, + #[serde( + rename(serialize = "@dstmacaddr"), + skip_serializing_if = "Option::is_none" + )] + dstmacaddr: Option, + #[serde( + rename(serialize = "@dstmacmask"), + skip_serializing_if = "Option::is_none" + )] + dstmacmask: Option, + #[serde( + rename(serialize = "@srcipaddr"), + skip_serializing_if = "Option::is_none" + )] + srcipaddr: Option, + #[serde( + rename(serialize = "@srcipmask"), + skip_serializing_if = "Option::is_none" + )] + srcipmask: Option, + #[serde( + rename(serialize = "@dstipaddr"), + skip_serializing_if = "Option::is_none" + )] + dstipaddr: Option, + #[serde( + rename(serialize = "@dstipmask"), + skip_serializing_if = "Option::is_none" + )] + dstipmask: Option, + + #[serde( + rename(serialize = "@comment"), + skip_serializing_if = "Option::is_none" + )] + comment: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename = "layer4")] +pub struct NetworkFilterRuleProtocolLayer4 { + #[serde( + rename(serialize = "@srcmacaddr"), + skip_serializing_if = "Option::is_none" + )] + srcmacaddr: Option, + #[serde( + rename(serialize = "@srcipaddr"), + skip_serializing_if = "Option::is_none" + )] + srcipaddr: Option, + #[serde( + rename(serialize = "@srcipmask"), + skip_serializing_if = "Option::is_none" + )] + srcipmask: Option, + #[serde( + rename(serialize = "@dstipaddr"), + skip_serializing_if = "Option::is_none" + )] + dstipaddr: Option, + #[serde( + rename(serialize = "@dstipmask"), + skip_serializing_if = "Option::is_none" + )] + dstipmask: Option, + /// Start of range of source IP address + #[serde( + rename(serialize = "@srcipfrom"), + skip_serializing_if = "Option::is_none" + )] + srcipfrom: Option, + /// End of range of source IP address + #[serde( + rename(serialize = "@srcipto"), + skip_serializing_if = "Option::is_none" + )] + srcipto: Option, + /// Start of range of destination IP address + #[serde( + rename(serialize = "@dstipfrom"), + skip_serializing_if = "Option::is_none" + )] + dstipfrom: Option, + /// End of range of destination IP address + #[serde( + rename(serialize = "@dstipto"), + skip_serializing_if = "Option::is_none" + )] + dstipto: Option, + #[serde( + rename(serialize = "@srcportstart"), + skip_serializing_if = "Option::is_none" + )] + srcportstart: Option, + #[serde( + rename(serialize = "@srcportend"), + skip_serializing_if = "Option::is_none" + )] + srcportend: Option, + #[serde( + rename(serialize = "@dstportstart"), + skip_serializing_if = "Option::is_none" + )] + dstportstart: Option, + #[serde( + rename(serialize = "@dstportend"), + skip_serializing_if = "Option::is_none" + )] + dstportend: Option, + #[serde(rename(serialize = "@state"), skip_serializing_if = "Option::is_none")] + state: Option, + + #[serde( + rename(serialize = "@comment"), + skip_serializing_if = "Option::is_none" + )] + comment: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename = "rule")] +pub struct NetworkFilterRuleXML { + #[serde(rename(serialize = "@action"))] + pub action: String, + #[serde(rename(serialize = "@direction"))] + pub direction: String, + #[serde(rename(serialize = "@priority"))] + pub priority: Option, + + /// Match all protocols + #[serde(skip_serializing_if = "Option::is_none")] + pub all: Option, + + /// Match mac protocol + #[serde(default, rename = "mac", skip_serializing_if = "Vec::is_empty")] + pub mac_rules: Vec, + + /// Match arp protocol + #[serde(default, rename = "arp", skip_serializing_if = "Vec::is_empty")] + pub arp_rules: Vec, + + /// Match IPv4 protocol + #[serde(default, rename = "ip", skip_serializing_if = "Vec::is_empty")] + pub ipv4_rules: Vec, + + /// Match IPv6 protocol + #[serde(default, rename = "ipv6", skip_serializing_if = "Vec::is_empty")] + pub ipv6_rules: Vec, + + /// Match TCP protocol + #[serde(default, rename = "tcp", skip_serializing_if = "Vec::is_empty")] + pub tcp_rules: Vec, + + /// Match UDP protocol + #[serde(default, rename = "udp", skip_serializing_if = "Vec::is_empty")] + pub udp_rules: Vec, + + /// Match SCTP protocol + #[serde(default, rename = "sctp", skip_serializing_if = "Vec::is_empty")] + pub sctp_rules: Vec, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename = "filter")] +pub struct NetworkFilterXML { + #[serde(rename(serialize = "@name"))] + pub name: String, + #[serde(rename(serialize = "@chain"), default)] + pub chain: String, + #[serde( + skip_serializing_if = "Option::is_none", + rename(serialize = "@priority"), + default + )] + pub priority: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub uuid: Option, + #[serde(default, rename = "filterref", skip_serializing_if = "Vec::is_empty")] + pub filterrefs: Vec, + #[serde(default, rename = "rule", skip_serializing_if = "Vec::is_empty")] + pub rules: Vec, +} + +impl NetworkFilterXML { + pub fn parse_xml(xml: D) -> anyhow::Result { + let xml = xml.to_string(); + + // We need to put all filter refs at the same location + let mut filter_refs = Vec::new(); + let xml = lazy_regex::regex_replace_all!(r#""#, &xml, |r: &str| { + filter_refs.push(r.to_string()); + + if r.contains('\n') { + log::warn!("A filterref contain a new line. This is a symptom of a new unsupported child attribute of object!"); + } + + "" + }); + + let filter_refs = filter_refs.join("\n"); + let xml = xml.replace("", &format!("{filter_refs}")); + log::debug!("Effective NW filter rule parsed: {xml}"); + + Ok(serde_xml_rs::from_str(&xml)?) + } +} diff --git a/virtweb_backend/src/libvirt_rest_structures.rs b/virtweb_backend/src/libvirt_rest_structures.rs deleted file mode 100644 index 41ac39f..0000000 --- a/virtweb_backend/src/libvirt_rest_structures.rs +++ /dev/null @@ -1,961 +0,0 @@ -use crate::app_config::AppConfig; -use crate::constants; -use crate::libvirt_lib_structures::{ - DevicesXML, DiskBootXML, DiskDriverXML, DiskReadOnlyXML, DiskSourceXML, DiskTargetXML, DiskXML, - DomainCPUTopology, DomainCPUXML, DomainInputXML, DomainMemoryXML, DomainNetInterfaceXML, - DomainVCPUXML, DomainXML, FeaturesXML, GraphicsXML, NetIntModelXML, NetIntSourceXML, - NetMacAddress, NetworkBridgeXML, NetworkDHCPHostXML, NetworkDHCPRangeXML, NetworkDHCPXML, - NetworkDNSForwarderXML, NetworkDNSXML, NetworkDomainXML, NetworkFilterXML, NetworkForwardXML, - NetworkIPXML, NetworkXML, OSLoaderXML, OSTypeXML, TPMBackendXML, TPMDeviceXML, VideoModelXML, - VideoXML, XMLUuid, ACPIXML, OSXML, -}; -use crate::libvirt_rest_structures::LibVirtStructError::StructureExtraction; -use crate::utils::disks_utils::Disk; -use crate::utils::files_utils; -use ipnetwork::{Ipv4Network, Ipv6Network}; -use lazy_regex::regex; -use num::Integer; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; -use std::ops::{Div, Mul}; - -// TODO : split into multiple files - -#[derive(thiserror::Error, Debug)] -enum LibVirtStructError { - #[error("StructureExtractionError: {0}")] - StructureExtraction(&'static str), - #[error("DomainExtractionError: {0}")] - DomainExtraction(String), - #[error("MBConvertError: {0}")] - MBConvert(String), - #[error("ParseFilteringChain: {0}")] - ParseFilteringChain(String), -} - -#[derive(serde::Serialize)] -pub struct HypervisorInfo { - pub r#type: String, - pub hyp_version: u32, - pub lib_version: u32, - pub capabilities: String, - pub free_memory: u64, - pub hostname: String, - pub node: HypervisorNodeInfo, -} - -#[derive(serde::Serialize)] -pub struct HypervisorNodeInfo { - pub cpu_model: String, - /// Memory size in kilobytes - pub memory_size: u64, - pub number_of_active_cpus: u32, - pub cpu_frequency_mhz: u32, - pub number_of_numa_cell: u32, - pub number_of_cpu_socket_per_node: u32, - pub number_of_core_per_sockets: u32, - pub number_of_threads_per_core: u32, -} - -#[derive(serde::Serialize, serde::Deserialize)] -pub enum BootType { - UEFI, - UEFISecureBoot, -} - -#[derive(serde::Serialize, serde::Deserialize)] -pub enum VMArchitecture { - #[serde(rename = "i686")] - I686, - #[serde(rename = "x86_64")] - X86_64, -} - -#[derive(serde::Serialize, serde::Deserialize)] -pub struct Network { - mac: String, - #[serde(flatten)] - r#type: NetworkType, -} - -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(tag = "type")] -pub enum NetworkType { - UserspaceSLIRPStack, - DefinedNetwork { network: String }, // TODO : complete network types -} - -#[derive(serde::Serialize, serde::Deserialize)] -pub struct VMInfo { - /// VM name (alphanumeric characters only) - pub name: String, - pub uuid: Option, - pub genid: Option, - pub title: Option, - pub description: Option, - pub boot_type: BootType, - pub architecture: VMArchitecture, - /// VM allocated memory, in megabytes - pub memory: usize, - /// Number of vCPU for the VM - pub number_vcpu: usize, - /// Enable VNC access through admin console - pub vnc_access: bool, - /// Attach ISO file(s) - pub iso_files: Vec, - /// 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, - /// Network cards - pub networks: Vec, - /// Add a TPM v2.0 module - pub tpm_module: bool, -} - -impl VMInfo { - /// Turn this VM into a domain - pub fn as_tomain(&self) -> anyhow::Result { - if !regex!("^[a-zA-Z0-9]+$").is_match(&self.name) { - return Err(StructureExtraction("VM name is invalid!").into()); - } - - let uuid = if let Some(n) = self.uuid { - if !n.is_valid() { - return Err(StructureExtraction("VM UUID is invalid!").into()); - } - n - } else { - XMLUuid::new_random() - }; - - if let Some(n) = &self.genid { - if !n.is_valid() { - return Err(StructureExtraction("VM genid is invalid!").into()); - } - } - - if let Some(n) = &self.title { - if n.contains('\n') { - return Err(StructureExtraction("VM title contain newline char!").into()); - } - } - - if self.memory < constants::MIN_VM_MEMORY || self.memory > constants::MAX_VM_MEMORY { - return Err(StructureExtraction("VM memory is invalid!").into()); - } - - if self.number_vcpu == 0 || (self.number_vcpu != 1 && self.number_vcpu.is_odd()) { - return Err(StructureExtraction("Invalid number of vCPU specified!").into()); - } - - let mut disks = vec![]; - - for iso_file in &self.iso_files { - if !files_utils::check_file_name(iso_file) { - return Err(StructureExtraction("ISO filename is invalid!").into()); - } - - let path = AppConfig::get().iso_storage_path().join(iso_file); - - if !path.exists() { - return Err(StructureExtraction("Specified ISO file does not exists!").into()); - } - - disks.push(DiskXML { - r#type: "file".to_string(), - device: "cdrom".to_string(), - driver: DiskDriverXML { - name: "qemu".to_string(), - r#type: "raw".to_string(), - cache: "none".to_string(), - }, - source: DiskSourceXML { - file: path.to_string_lossy().to_string(), - }, - target: DiskTargetXML { - dev: format!( - "hd{}", - ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"][disks.len()] - ), - bus: "sata".to_string(), - }, - readonly: Some(DiskReadOnlyXML {}), - boot: DiskBootXML { - order: (disks.len() + 1).to_string(), - }, - address: None, - }) - } - - let (vnc_graphics, vnc_video) = match self.vnc_access { - true => ( - Some(GraphicsXML { - r#type: "vnc".to_string(), - socket: AppConfig::get() - .vnc_socket_for_domain(&self.name) - .to_string_lossy() - .to_string(), - }), - Some(VideoXML { - model: VideoModelXML { - r#type: "virtio".to_string(), //"qxl".to_string(), - }, - }), - ), - false => (None, None), - }; - - // Check disks name for duplicates - for disk in &self.disks { - if self.disks.iter().filter(|d| d.name == disk.name).count() > 1 { - return Err(StructureExtraction("Two different disks have the same name!").into()); - } - } - - // Apply disks configuration - for disk in &self.disks { - disk.check_config()?; - disk.apply_config(uuid)?; - - if disk.delete { - continue; - } - - disks.push(DiskXML { - r#type: "file".to_string(), - device: "disk".to_string(), - driver: DiskDriverXML { - name: "qemu".to_string(), - r#type: "raw".to_string(), - cache: "none".to_string(), - }, - source: DiskSourceXML { - file: disk.disk_path(uuid).to_string_lossy().to_string(), - }, - target: DiskTargetXML { - dev: format!( - "vd{}", - ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"][disks.len()] - ), - bus: "virtio".to_string(), - }, - readonly: None, - boot: DiskBootXML { - order: (disks.len() + 1).to_string(), - }, - address: None, - }) - } - - let mut networks = vec![]; - for n in &self.networks { - networks.push(match &n.r#type { - NetworkType::UserspaceSLIRPStack => DomainNetInterfaceXML { - mac: NetMacAddress { - address: n.mac.to_string(), - }, - r#type: "user".to_string(), - source: None, - model: Some(NetIntModelXML { - r#type: "virtio".to_string(), - }), - }, - NetworkType::DefinedNetwork { network } => DomainNetInterfaceXML { - mac: NetMacAddress { - address: n.mac.to_string(), - }, - r#type: "network".to_string(), - source: Some(NetIntSourceXML { - network: network.to_string(), - }), - model: Some(NetIntModelXML { - r#type: "virtio".to_string(), - }), - }, - }) - } - - Ok(DomainXML { - r#type: "kvm".to_string(), - name: self.name.to_string(), - uuid: Some(uuid), - genid: self.genid.map(|i| i.0), - title: self.title.clone(), - description: self.description.clone(), - - os: OSXML { - r#type: OSTypeXML { - arch: match self.architecture { - VMArchitecture::I686 => "i686", - VMArchitecture::X86_64 => "x86_64", - } - .to_string(), - machine: "q35".to_string(), - body: "hvm".to_string(), - }, - firmware: "efi".to_string(), - loader: Some(OSLoaderXML { - secure: match self.boot_type { - BootType::UEFI => "no".to_string(), - BootType::UEFISecureBoot => "yes".to_string(), - }, - }), - }, - - features: FeaturesXML { acpi: ACPIXML {} }, - - devices: DevicesXML { - graphics: vnc_graphics, - video: vnc_video, - disks, - net_interfaces: networks, - inputs: vec![ - DomainInputXML { - r#type: "mouse".to_string(), - }, - DomainInputXML { - r#type: "keyboard".to_string(), - }, - DomainInputXML { - r#type: "tablet".to_string(), - }, - ], - tpm: match self.tpm_module { - true => Some(TPMDeviceXML { - model: "tpm-tis".to_string(), - backend: TPMBackendXML { - r#type: "emulator".to_string(), - version: "2.0".to_string(), - }, - }), - false => None, - }, - }, - - memory: DomainMemoryXML { - unit: "MB".to_string(), - memory: self.memory, - }, - - vcpu: DomainVCPUXML { - body: self.number_vcpu, - }, - - cpu: DomainCPUXML { - mode: "host-passthrough".to_string(), - topology: Some(DomainCPUTopology { - sockets: 1, - cores: match self.number_vcpu { - 1 => 1, - v => v / 2, - }, - threads: match self.number_vcpu { - 1 => 1, - _ => 2, - }, - }), - }, - - on_poweroff: "destroy".to_string(), - on_reboot: "restart".to_string(), - on_crash: "destroy".to_string(), - }) - } - - /// Turn a domain into a vm - pub fn from_domain(domain: DomainXML) -> anyhow::Result { - Ok(Self { - name: domain.name, - uuid: domain.uuid, - genid: domain.genid.map(XMLUuid), - title: domain.title, - description: domain.description, - boot_type: match domain.os.loader { - None => BootType::UEFI, - Some(l) => match l.secure.as_str() { - "yes" => BootType::UEFISecureBoot, - _ => BootType::UEFI, - }, - }, - architecture: match domain.os.r#type.arch.as_str() { - "i686" => VMArchitecture::I686, - "x86_64" => VMArchitecture::X86_64, - a => { - return Err(LibVirtStructError::DomainExtraction(format!( - "Unknown architecture: {a}! " - )) - .into()); - } - }, - number_vcpu: domain.vcpu.body, - memory: convert_to_mb(&domain.memory.unit, domain.memory.memory)?, - vnc_access: domain.devices.graphics.is_some(), - iso_files: domain - .devices - .disks - .iter() - .filter(|d| d.device == "cdrom") - .map(|d| d.source.file.rsplit_once('/').unwrap().1.to_string()) - .collect(), - - disks: domain - .devices - .disks - .iter() - .filter(|d| d.device == "disk") - .map(|d| Disk::load_from_file(&d.source.file).unwrap()) - .collect(), - - networks: domain - .devices - .net_interfaces - .iter() - .map(|d| { - Ok(Network { - 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(), - }, - a => { - return Err(LibVirtStructError::DomainExtraction(format!( - "Unknown network interface type: {a}! " - ))); - } - }, - }) - }) - .collect::, _>>()?, - - tpm_module: domain.devices.tpm.is_some(), - }) - } -} - -/// Convert unit to MB -fn convert_to_mb(unit: &str, value: usize) -> anyhow::Result { - let fact = match unit { - "bytes" | "b" => 1f64, - "KB" => 1000f64, - "MB" => 1000f64 * 1000f64, - "GB" => 1000f64 * 1000f64 * 1000f64, - "TB" => 1000f64 * 1000f64 * 1000f64 * 1000f64, - - "k" | "KiB" => 1024f64, - "M" | "MiB" => 1024f64 * 1024f64, - "G" | "GiB" => 1024f64 * 1024f64 * 1024f64, - "T" | "TiB" => 1024f64 * 1024f64 * 1024f64 * 1024f64, - - _ => { - return Err(LibVirtStructError::MBConvert(format!("Unknown size unit: {unit}")).into()); - } - }; - - Ok((value as f64).mul(fact.div((1000 * 1000) as f64)).ceil() as usize) -} - -#[derive(serde::Serialize, serde::Deserialize, Copy, Clone, Debug)] -pub enum NetworkForwardMode { - NAT, - Isolated, -} - -#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] -pub struct DHCPv4HostReservation { - mac: String, - name: String, - ip: Ipv4Addr, -} - -#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] -pub struct IPv4DHCPConfig { - start: Ipv4Addr, - end: Ipv4Addr, - hosts: Vec, -} - -#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] -pub struct IPV4Config { - bridge_address: Ipv4Addr, - prefix: u32, - dhcp: Option, -} - -#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] -pub struct DHCPv6HostReservation { - name: String, - ip: Ipv6Addr, -} - -#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] -pub struct IPv6DHCPConfig { - start: Ipv6Addr, - end: Ipv6Addr, - hosts: Vec, -} - -#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] -pub struct IPV6Config { - bridge_address: Ipv6Addr, - prefix: u32, - dhcp: Option, -} - -/// Network configuration -#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] -pub struct NetworkInfo { - pub name: String, - pub uuid: Option, - title: Option, - description: Option, - forward_mode: NetworkForwardMode, - device: Option, - bridge_name: Option, - dns_server: Option, - domain: Option, - ip_v4: Option, - ip_v6: Option, -} - -impl NetworkInfo { - pub fn as_virt_network(&self) -> anyhow::Result { - if !regex!("^[a-zA-Z0-9]+$").is_match(&self.name) { - return Err(StructureExtraction("network name is invalid!").into()); - } - - if let Some(n) = &self.title { - if n.contains('\n') { - return Err(StructureExtraction("Network title contain newline char!").into()); - } - } - - if let Some(dev) = &self.device { - if !regex!("^[a-zA-Z0-9]+$").is_match(dev) { - return Err(StructureExtraction("Network device name is invalid!").into()); - } - } - - if let Some(bridge) = &self.bridge_name { - if !regex!("^[a-zA-Z0-9]+$").is_match(bridge) { - return Err(StructureExtraction("Network bridge name is invalid!").into()); - } - } - - if let Some(domain) = &self.domain { - if !regex!("^[a-zA-Z0-9.]+$").is_match(domain) { - return Err(StructureExtraction("Domain name is invalid!").into()); - } - } - - let mut ips = Vec::with_capacity(2); - - if let Some(ipv4) = &self.ip_v4 { - if ipv4.prefix > 32 { - return Err(StructureExtraction("IPv4 prefix is invalid!").into()); - } - - ips.push(NetworkIPXML { - family: "ipv4".to_string(), - address: IpAddr::V4(ipv4.bridge_address), - prefix: ipv4.prefix, - netmask: Ipv4Network::new(ipv4.bridge_address, ipv4.prefix as u8) - .unwrap() - .mask() - .into(), - dhcp: ipv4.dhcp.as_ref().map(|dhcp| NetworkDHCPXML { - range: NetworkDHCPRangeXML { - start: IpAddr::V4(dhcp.start), - end: IpAddr::V4(dhcp.end), - }, - hosts: dhcp - .hosts - .iter() - .map(|c| NetworkDHCPHostXML { - mac: c.mac.to_string(), - name: c.name.to_string(), - ip: c.ip.into(), - }) - .collect::>(), - }), - }) - } - - if let Some(ipv6) = &self.ip_v6 { - ips.push(NetworkIPXML { - family: "ipv6".to_string(), - address: IpAddr::V6(ipv6.bridge_address), - prefix: ipv6.prefix, - netmask: Ipv6Network::new(ipv6.bridge_address, ipv6.prefix as u8) - .unwrap() - .mask() - .into(), - dhcp: ipv6.dhcp.as_ref().map(|dhcp| NetworkDHCPXML { - range: NetworkDHCPRangeXML { - start: IpAddr::V6(dhcp.start), - end: IpAddr::V6(dhcp.end), - }, - hosts: dhcp - .hosts - .iter() - .map(|h| NetworkDHCPHostXML { - mac: "".to_string(), - name: h.name.to_string(), - ip: h.ip.into(), - }) - .collect(), - }), - }) - } - - Ok(NetworkXML { - name: self.name.to_string(), - uuid: self.uuid, - title: self.title.clone(), - description: self.description.clone(), - forward: match self.forward_mode { - NetworkForwardMode::NAT => Some(NetworkForwardXML { - mode: "nat".to_string(), - dev: self.device.clone().unwrap_or_default(), - }), - NetworkForwardMode::Isolated => None, - }, - bridge: self.bridge_name.clone().map(|b| NetworkBridgeXML { - name: b.to_string(), - }), - dns: self.dns_server.map(|addr| NetworkDNSXML { - forwarder: NetworkDNSForwarderXML { addr }, - }), - domain: self.domain.clone().map(|name| NetworkDomainXML { name }), - ips, - }) - } - - pub fn from_xml(xml: NetworkXML) -> anyhow::Result { - Ok(Self { - name: xml.name, - uuid: xml.uuid, - title: xml.title, - description: xml.description, - forward_mode: match xml.forward { - None => NetworkForwardMode::Isolated, - Some(_) => NetworkForwardMode::NAT, - }, - device: xml - .forward - .map(|f| match f.dev.is_empty() { - true => None, - false => Some(f.dev), - }) - .unwrap_or(None), - bridge_name: xml.bridge.map(|b| b.name), - dns_server: xml.dns.map(|d| d.forwarder.addr), - domain: xml.domain.map(|d| d.name), - ip_v4: xml - .ips - .iter() - .find(|i| i.family != "ipv6") - .map(|i| IPV4Config { - bridge_address: extract_ipv4(i.address), - prefix: match i.prefix { - u32::MAX => ipnetwork::ipv4_mask_to_prefix(extract_ipv4(i.netmask)) - .expect("Failed to convert IPv4 netmask to network") - as u32, - p => p, - }, - dhcp: i.dhcp.as_ref().map(|d| IPv4DHCPConfig { - start: extract_ipv4(d.range.start), - end: extract_ipv4(d.range.end), - hosts: d - .hosts - .iter() - .map(|h| DHCPv4HostReservation { - mac: h.mac.to_string(), - name: h.name.to_string(), - ip: extract_ipv4(h.ip), - }) - .collect(), - }), - }), - ip_v6: xml - .ips - .iter() - .find(|i| i.family == "ipv6") - .map(|i| IPV6Config { - bridge_address: extract_ipv6(i.address), - prefix: match i.prefix { - u32::MAX => ipnetwork::ipv6_mask_to_prefix(extract_ipv6(i.netmask)) - .expect("Failed to convert IPv6 netmask to network") - as u32, - p => p, - }, - dhcp: i.dhcp.as_ref().map(|d| IPv6DHCPConfig { - start: extract_ipv6(d.range.start), - end: extract_ipv6(d.range.end), - hosts: d - .hosts - .iter() - .map(|h| DHCPv6HostReservation { - name: h.name.to_string(), - ip: extract_ipv6(h.ip), - }) - .collect(), - }), - }), - }) - } -} - -#[derive(serde::Serialize, serde::Deserialize, Debug, Copy, Clone)] -pub enum NetworkFilterChainProtocol { - Root, - Mac, - STP, - VLAN, - ARP, - RARP, - IPv4, - IPv6, -} - -impl NetworkFilterChainProtocol { - pub fn from_xml(xml: &str) -> anyhow::Result { - Ok(match xml { - "root" => Self::Root, - "mac" => Self::Mac, - "stp" => Self::STP, - "vlan" => Self::VLAN, - "arp" => Self::ARP, - "rarp" => Self::RARP, - "ipv4" => Self::IPv4, - "ipv6" => Self::IPv6, - _ => { - return Err(LibVirtStructError::ParseFilteringChain(format!( - "Unknown filtering chain: {xml}! " - )) - .into()) - } - }) - } - - pub fn to_xml(&self) -> String { - match self { - Self::Root => "root", - Self::Mac => "mac", - Self::STP => "stp", - Self::VLAN => "vlan", - Self::ARP => "arp", - Self::RARP => "rarp", - Self::IPv4 => "ipv4", - Self::IPv6 => "ipv6", - } - .to_string() - } -} - -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct NetworkFilterChain { - protocol: NetworkFilterChainProtocol, - suffix: Option, -} - -impl NetworkFilterChain { - pub fn from_xml(xml: &str) -> anyhow::Result> { - if xml.is_empty() { - return Ok(None); - } - - Ok(Some(match xml.split_once('-') { - None => Self { - protocol: NetworkFilterChainProtocol::from_xml(xml)?, - suffix: None, - }, - Some((prefix, suffix)) => Self { - protocol: NetworkFilterChainProtocol::from_xml(prefix)?, - suffix: Some(suffix.to_string()), - }, - })) - } - - pub fn to_xml(&self) -> String { - match &self.suffix { - None => self.protocol.to_xml(), - Some(s) => format!("{}-{s}", self.protocol.to_xml()), - } - } -} - -/// Network filter definition -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct NetworkFilter { - name: String, - chain: Option, - priority: Option, - uuid: Option, - /// Referenced filters rules - join_rules: Vec, - rules: Vec, -} - -impl NetworkFilter { - pub fn from_xml(xml: NetworkFilterXML) -> anyhow::Result { - Ok(Self { - name: xml.name, - uuid: xml.uuid, - chain: NetworkFilterChain::from_xml(&xml.chain)?, - priority: xml.priority, - join_rules: xml - .filterrefs - .iter() - .map(|i| i.filter.to_string()) - .collect(), - rules: vec![], // TODO ! - }) - } -} - -#[derive(serde::Serialize, serde::Deserialize, Debug, Copy, Clone)] -pub enum NetworkFilterAction { - /// matching the rule silently discards the packet with no further analysis - Drop, - /// matching the rule generates an ICMP reject message with no further analysis - Reject, - /// matching the rule accepts the packet with no further analysis - Accept, - /// matching the rule passes this filter, but returns control to the calling filter for further - /// analysis - Return, - /// matching the rule goes on to the next rule for further analysis - Continue, -} - -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub enum NetworkFilterDirection { - In, - Out, - InOut, -} - -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub enum Layer4State { - NEW, - ESTABLISHED, - RELATED, - INVALID, - NONE, -} - -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub enum Layer4Type { - TCP, - UDP, - SCTP, - ICMP, - TCPipv6, - UDPipv6, - SCTPipv6, - ICMPipv6, -} - -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct NetworkFilterSelectorIP { - srcmacaddr: Option, - srcmacmask: Option, - dstmacaddr: Option, - dstmacmask: Option, - srcipaddr: Option, - srcipmask: Option, - dstipaddr: Option, - dstipmask: Option, - comment: Option, -} - -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub enum NetworkFilterSelector { - All, - Mac { - src_mac_addr: Option, - src_mac_mask: Option, - dst_mac_addr: Option, - dst_mac_mask: Option, - comment: Option, - }, - Arp { - srcmacaddr: Option, - srcmacmask: Option, - dstmacaddr: Option, - dstmacmask: Option, - arpsrcipaddr: Option, - arpsrcipmask: Option, - arpdstipaddr: Option, - arpdstipmask: Option, - comment: Option, - }, - IPv4(NetworkFilterSelectorIP), - IPv6(NetworkFilterSelectorIP), - Layer4 { - r#type: Layer4Type, - srcmacaddr: Option, - srcipaddr: Option, - srcipmask: Option, - dstipaddr: Option, - dstipmask: Option, - /// Start of range of source IP address - srcipfrom: Option, - /// End of range of source IP address - srcipto: Option, - /// Start of range of destination IP address - dstipfrom: Option, - /// End of range of destination IP address - dstipto: Option, - srcportstart: Option, - srcportend: Option, - dstportstart: Option, - dstportend: Option, - state: Option, - comment: Option, - }, -} - -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct NetworkFilterRule { - action: NetworkFilterAction, - direction: NetworkFilterDirection, - /// optional; the priority of the rule controls the order in which the rule will be instantiated - /// relative to other rules - /// - /// Valid values are in the range of -1000 to 1000. - priority: Option, - selectors: Vec, -} - -fn extract_ipv4(ip: IpAddr) -> Ipv4Addr { - match ip { - IpAddr::V4(i) => i, - IpAddr::V6(_) => { - panic!("IPv6 found in IPv4 definition!") - } - } -} - -fn extract_ipv6(ip: IpAddr) -> Ipv6Addr { - match ip { - IpAddr::V4(_) => { - panic!("IPv4 found in IPv6 definition!") - } - IpAddr::V6(i) => i, - } -} - -#[cfg(test)] -mod test { - use crate::libvirt_rest_structures::convert_to_mb; - - #[test] - fn convert_units_mb() { - assert_eq!(convert_to_mb("MB", 1).unwrap(), 1); - assert_eq!(convert_to_mb("MB", 1000).unwrap(), 1000); - assert_eq!(convert_to_mb("GB", 1000).unwrap(), 1000 * 1000); - assert_eq!(convert_to_mb("GB", 1).unwrap(), 1000); - assert_eq!(convert_to_mb("GiB", 3).unwrap(), 3222); - assert_eq!(convert_to_mb("KiB", 488281).unwrap(), 500); - } -} diff --git a/virtweb_backend/src/libvirt_rest_structures/hypervisor.rs b/virtweb_backend/src/libvirt_rest_structures/hypervisor.rs new file mode 100644 index 0000000..4252399 --- /dev/null +++ b/virtweb_backend/src/libvirt_rest_structures/hypervisor.rs @@ -0,0 +1,23 @@ +#[derive(serde::Serialize)] +pub struct HypervisorInfo { + pub r#type: String, + pub hyp_version: u32, + pub lib_version: u32, + pub capabilities: String, + pub free_memory: u64, + pub hostname: String, + pub node: HypervisorNodeInfo, +} + +#[derive(serde::Serialize)] +pub struct HypervisorNodeInfo { + pub cpu_model: String, + /// Memory size in kilobytes + pub memory_size: u64, + pub number_of_active_cpus: u32, + pub cpu_frequency_mhz: u32, + pub number_of_numa_cell: u32, + pub number_of_cpu_socket_per_node: u32, + pub number_of_core_per_sockets: u32, + pub number_of_threads_per_core: u32, +} diff --git a/virtweb_backend/src/libvirt_rest_structures/mod.rs b/virtweb_backend/src/libvirt_rest_structures/mod.rs new file mode 100644 index 0000000..cb17c8c --- /dev/null +++ b/virtweb_backend/src/libvirt_rest_structures/mod.rs @@ -0,0 +1,15 @@ +pub mod hypervisor; +pub mod net; +pub mod nw_filter; +pub mod vm; + +#[derive(thiserror::Error, Debug)] +enum LibVirtStructError { + #[error("StructureExtractionError: {0}")] + StructureExtraction(&'static str), + #[error("DomainExtractionError: {0}")] + DomainExtraction(String), + + #[error("ParseFilteringChain: {0}")] + ParseFilteringChain(String), +} diff --git a/virtweb_backend/src/libvirt_rest_structures/net.rs b/virtweb_backend/src/libvirt_rest_structures/net.rs new file mode 100644 index 0000000..64e6d86 --- /dev/null +++ b/virtweb_backend/src/libvirt_rest_structures/net.rs @@ -0,0 +1,258 @@ +use crate::libvirt_lib_structures::network::*; +use crate::libvirt_lib_structures::XMLUuid; +use crate::libvirt_rest_structures::LibVirtStructError::StructureExtraction; +use crate::utils::net_utils::{extract_ipv4, extract_ipv6}; +use ipnetwork::{Ipv4Network, Ipv6Network}; +use lazy_regex::regex; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +#[derive(serde::Serialize, serde::Deserialize, Copy, Clone, Debug)] +pub enum NetworkForwardMode { + NAT, + Isolated, +} + +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] +pub struct DHCPv4HostReservation { + mac: String, + name: String, + ip: Ipv4Addr, +} + +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] +pub struct IPv4DHCPConfig { + start: Ipv4Addr, + end: Ipv4Addr, + hosts: Vec, +} + +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] +pub struct IPV4Config { + bridge_address: Ipv4Addr, + prefix: u32, + dhcp: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] +pub struct DHCPv6HostReservation { + name: String, + ip: Ipv6Addr, +} + +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] +pub struct IPv6DHCPConfig { + start: Ipv6Addr, + end: Ipv6Addr, + hosts: Vec, +} + +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] +pub struct IPV6Config { + bridge_address: Ipv6Addr, + prefix: u32, + dhcp: Option, +} + +/// Network configuration +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] +pub struct NetworkInfo { + pub name: String, + pub uuid: Option, + title: Option, + description: Option, + forward_mode: NetworkForwardMode, + device: Option, + bridge_name: Option, + dns_server: Option, + domain: Option, + ip_v4: Option, + ip_v6: Option, +} + +impl NetworkInfo { + pub fn as_virt_network(&self) -> anyhow::Result { + if !regex!("^[a-zA-Z0-9]+$").is_match(&self.name) { + return Err(StructureExtraction("network name is invalid!").into()); + } + + if let Some(n) = &self.title { + if n.contains('\n') { + return Err(StructureExtraction("Network title contain newline char!").into()); + } + } + + if let Some(dev) = &self.device { + if !regex!("^[a-zA-Z0-9]+$").is_match(dev) { + return Err(StructureExtraction("Network device name is invalid!").into()); + } + } + + if let Some(bridge) = &self.bridge_name { + if !regex!("^[a-zA-Z0-9]+$").is_match(bridge) { + return Err(StructureExtraction("Network bridge name is invalid!").into()); + } + } + + if let Some(domain) = &self.domain { + if !regex!("^[a-zA-Z0-9.]+$").is_match(domain) { + return Err(StructureExtraction("Domain name is invalid!").into()); + } + } + + let mut ips = Vec::with_capacity(2); + + if let Some(ipv4) = &self.ip_v4 { + if ipv4.prefix > 32 { + return Err(StructureExtraction("IPv4 prefix is invalid!").into()); + } + + ips.push(NetworkIPXML { + family: "ipv4".to_string(), + address: IpAddr::V4(ipv4.bridge_address), + prefix: ipv4.prefix, + netmask: Ipv4Network::new(ipv4.bridge_address, ipv4.prefix as u8) + .unwrap() + .mask() + .into(), + dhcp: ipv4.dhcp.as_ref().map(|dhcp| NetworkDHCPXML { + range: NetworkDHCPRangeXML { + start: IpAddr::V4(dhcp.start), + end: IpAddr::V4(dhcp.end), + }, + hosts: dhcp + .hosts + .iter() + .map(|c| NetworkDHCPHostXML { + mac: c.mac.to_string(), + name: c.name.to_string(), + ip: c.ip.into(), + }) + .collect::>(), + }), + }) + } + + if let Some(ipv6) = &self.ip_v6 { + ips.push(NetworkIPXML { + family: "ipv6".to_string(), + address: IpAddr::V6(ipv6.bridge_address), + prefix: ipv6.prefix, + netmask: Ipv6Network::new(ipv6.bridge_address, ipv6.prefix as u8) + .unwrap() + .mask() + .into(), + dhcp: ipv6.dhcp.as_ref().map(|dhcp| NetworkDHCPXML { + range: NetworkDHCPRangeXML { + start: IpAddr::V6(dhcp.start), + end: IpAddr::V6(dhcp.end), + }, + hosts: dhcp + .hosts + .iter() + .map(|h| NetworkDHCPHostXML { + mac: "".to_string(), + name: h.name.to_string(), + ip: h.ip.into(), + }) + .collect(), + }), + }) + } + + Ok(NetworkXML { + name: self.name.to_string(), + uuid: self.uuid, + title: self.title.clone(), + description: self.description.clone(), + forward: match self.forward_mode { + NetworkForwardMode::NAT => Some(NetworkForwardXML { + mode: "nat".to_string(), + dev: self.device.clone().unwrap_or_default(), + }), + NetworkForwardMode::Isolated => None, + }, + bridge: self.bridge_name.clone().map(|b| NetworkBridgeXML { + name: b.to_string(), + }), + dns: self.dns_server.map(|addr| NetworkDNSXML { + forwarder: NetworkDNSForwarderXML { addr }, + }), + domain: self.domain.clone().map(|name| NetworkDomainXML { name }), + ips, + }) + } + + pub fn from_xml(xml: NetworkXML) -> anyhow::Result { + Ok(Self { + name: xml.name, + uuid: xml.uuid, + title: xml.title, + description: xml.description, + forward_mode: match xml.forward { + None => NetworkForwardMode::Isolated, + Some(_) => NetworkForwardMode::NAT, + }, + device: xml + .forward + .map(|f| match f.dev.is_empty() { + true => None, + false => Some(f.dev), + }) + .unwrap_or(None), + bridge_name: xml.bridge.map(|b| b.name), + dns_server: xml.dns.map(|d| d.forwarder.addr), + domain: xml.domain.map(|d| d.name), + ip_v4: xml + .ips + .iter() + .find(|i| i.family != "ipv6") + .map(|i| IPV4Config { + bridge_address: extract_ipv4(i.address), + prefix: match i.prefix { + u32::MAX => ipnetwork::ipv4_mask_to_prefix(extract_ipv4(i.netmask)) + .expect("Failed to convert IPv4 netmask to network") + as u32, + p => p, + }, + dhcp: i.dhcp.as_ref().map(|d| IPv4DHCPConfig { + start: extract_ipv4(d.range.start), + end: extract_ipv4(d.range.end), + hosts: d + .hosts + .iter() + .map(|h| DHCPv4HostReservation { + mac: h.mac.to_string(), + name: h.name.to_string(), + ip: extract_ipv4(h.ip), + }) + .collect(), + }), + }), + ip_v6: xml + .ips + .iter() + .find(|i| i.family == "ipv6") + .map(|i| IPV6Config { + bridge_address: extract_ipv6(i.address), + prefix: match i.prefix { + u32::MAX => ipnetwork::ipv6_mask_to_prefix(extract_ipv6(i.netmask)) + .expect("Failed to convert IPv6 netmask to network") + as u32, + p => p, + }, + dhcp: i.dhcp.as_ref().map(|d| IPv6DHCPConfig { + start: extract_ipv6(d.range.start), + end: extract_ipv6(d.range.end), + hosts: d + .hosts + .iter() + .map(|h| DHCPv6HostReservation { + name: h.name.to_string(), + ip: extract_ipv6(h.ip), + }) + .collect(), + }), + }), + }) + } +} diff --git a/virtweb_backend/src/libvirt_rest_structures/nw_filter.rs b/virtweb_backend/src/libvirt_rest_structures/nw_filter.rs new file mode 100644 index 0000000..5cc494c --- /dev/null +++ b/virtweb_backend/src/libvirt_rest_structures/nw_filter.rs @@ -0,0 +1,227 @@ +use crate::libvirt_lib_structures::nwfilter::NetworkFilterXML; +use crate::libvirt_lib_structures::XMLUuid; +use crate::libvirt_rest_structures::LibVirtStructError; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +#[derive(serde::Serialize, serde::Deserialize, Debug, Copy, Clone)] +pub enum NetworkFilterChainProtocol { + Root, + Mac, + STP, + VLAN, + ARP, + RARP, + IPv4, + IPv6, +} + +impl NetworkFilterChainProtocol { + pub fn from_xml(xml: &str) -> anyhow::Result { + Ok(match xml { + "root" => Self::Root, + "mac" => Self::Mac, + "stp" => Self::STP, + "vlan" => Self::VLAN, + "arp" => Self::ARP, + "rarp" => Self::RARP, + "ipv4" => Self::IPv4, + "ipv6" => Self::IPv6, + _ => { + return Err(LibVirtStructError::ParseFilteringChain(format!( + "Unknown filtering chain: {xml}! " + )) + .into()) + } + }) + } + + pub fn to_xml(&self) -> String { + match self { + Self::Root => "root", + Self::Mac => "mac", + Self::STP => "stp", + Self::VLAN => "vlan", + Self::ARP => "arp", + Self::RARP => "rarp", + Self::IPv4 => "ipv4", + Self::IPv6 => "ipv6", + } + .to_string() + } +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct NetworkFilterChain { + protocol: NetworkFilterChainProtocol, + suffix: Option, +} + +impl NetworkFilterChain { + pub fn from_xml(xml: &str) -> anyhow::Result> { + if xml.is_empty() { + return Ok(None); + } + + Ok(Some(match xml.split_once('-') { + None => Self { + protocol: NetworkFilterChainProtocol::from_xml(xml)?, + suffix: None, + }, + Some((prefix, suffix)) => Self { + protocol: NetworkFilterChainProtocol::from_xml(prefix)?, + suffix: Some(suffix.to_string()), + }, + })) + } + + pub fn to_xml(&self) -> String { + match &self.suffix { + None => self.protocol.to_xml(), + Some(s) => format!("{}-{s}", self.protocol.to_xml()), + } + } +} + +/// Network filter definition +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct NetworkFilter { + name: String, + chain: Option, + priority: Option, + uuid: Option, + /// Referenced filters rules + join_rules: Vec, + rules: Vec, +} + +impl NetworkFilter { + pub fn from_xml(xml: NetworkFilterXML) -> anyhow::Result { + Ok(Self { + name: xml.name, + uuid: xml.uuid, + chain: NetworkFilterChain::from_xml(&xml.chain)?, + priority: xml.priority, + join_rules: xml + .filterrefs + .iter() + .map(|i| i.filter.to_string()) + .collect(), + rules: vec![], // TODO ! + }) + } +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Copy, Clone)] +pub enum NetworkFilterAction { + /// matching the rule silently discards the packet with no further analysis + Drop, + /// matching the rule generates an ICMP reject message with no further analysis + Reject, + /// matching the rule accepts the packet with no further analysis + Accept, + /// matching the rule passes this filter, but returns control to the calling filter for further + /// analysis + Return, + /// matching the rule goes on to the next rule for further analysis + Continue, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub enum NetworkFilterDirection { + In, + Out, + InOut, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub enum Layer4State { + NEW, + ESTABLISHED, + RELATED, + INVALID, + NONE, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub enum Layer4Type { + TCP, + UDP, + SCTP, + ICMP, + TCPipv6, + UDPipv6, + SCTPipv6, + ICMPipv6, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct NetworkFilterSelectorIP { + srcmacaddr: Option, + srcmacmask: Option, + dstmacaddr: Option, + dstmacmask: Option, + srcipaddr: Option, + srcipmask: Option, + dstipaddr: Option, + dstipmask: Option, + comment: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub enum NetworkFilterSelector { + All, + Mac { + src_mac_addr: Option, + src_mac_mask: Option, + dst_mac_addr: Option, + dst_mac_mask: Option, + comment: Option, + }, + Arp { + srcmacaddr: Option, + srcmacmask: Option, + dstmacaddr: Option, + dstmacmask: Option, + arpsrcipaddr: Option, + arpsrcipmask: Option, + arpdstipaddr: Option, + arpdstipmask: Option, + comment: Option, + }, + IPv4(NetworkFilterSelectorIP), + IPv6(NetworkFilterSelectorIP), + Layer4 { + r#type: Layer4Type, + srcmacaddr: Option, + srcipaddr: Option, + srcipmask: Option, + dstipaddr: Option, + dstipmask: Option, + /// Start of range of source IP address + srcipfrom: Option, + /// End of range of source IP address + srcipto: Option, + /// Start of range of destination IP address + dstipfrom: Option, + /// End of range of destination IP address + dstipto: Option, + srcportstart: Option, + srcportend: Option, + dstportstart: Option, + dstportend: Option, + state: Option, + comment: Option, + }, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct NetworkFilterRule { + action: NetworkFilterAction, + direction: NetworkFilterDirection, + /// optional; the priority of the rule controls the order in which the rule will be instantiated + /// relative to other rules + /// + /// Valid values are in the range of -1000 to 1000. + priority: Option, + selectors: Vec, +} diff --git a/virtweb_backend/src/libvirt_rest_structures/vm.rs b/virtweb_backend/src/libvirt_rest_structures/vm.rs new file mode 100644 index 0000000..cad1bc9 --- /dev/null +++ b/virtweb_backend/src/libvirt_rest_structures/vm.rs @@ -0,0 +1,386 @@ +use crate::app_config::AppConfig; +use crate::constants; +use crate::libvirt_lib_structures::domain::*; +use crate::libvirt_lib_structures::XMLUuid; +use crate::libvirt_rest_structures::LibVirtStructError; +use crate::libvirt_rest_structures::LibVirtStructError::StructureExtraction; +use crate::utils::disks_utils::Disk; +use crate::utils::files_utils; +use crate::utils::files_utils::convert_size_unit_to_mb; +use lazy_regex::regex; +use num::Integer; + +#[derive(serde::Serialize, serde::Deserialize)] +pub enum BootType { + UEFI, + UEFISecureBoot, +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub enum VMArchitecture { + #[serde(rename = "i686")] + I686, + #[serde(rename = "x86_64")] + X86_64, +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct Network { + mac: String, + #[serde(flatten)] + r#type: NetworkType, +} + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(tag = "type")] +pub enum NetworkType { + UserspaceSLIRPStack, + DefinedNetwork { network: String }, // TODO : complete network types +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct VMInfo { + /// VM name (alphanumeric characters only) + pub name: String, + pub uuid: Option, + pub genid: Option, + pub title: Option, + pub description: Option, + pub boot_type: BootType, + pub architecture: VMArchitecture, + /// VM allocated memory, in megabytes + pub memory: usize, + /// Number of vCPU for the VM + pub number_vcpu: usize, + /// Enable VNC access through admin console + pub vnc_access: bool, + /// Attach ISO file(s) + pub iso_files: Vec, + /// 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, + /// Network cards + pub networks: Vec, + /// Add a TPM v2.0 module + pub tpm_module: bool, +} + +impl VMInfo { + /// Turn this VM into a domain + pub fn as_tomain(&self) -> anyhow::Result { + if !regex!("^[a-zA-Z0-9]+$").is_match(&self.name) { + return Err(StructureExtraction("VM name is invalid!").into()); + } + + let uuid = if let Some(n) = self.uuid { + if !n.is_valid() { + return Err(StructureExtraction("VM UUID is invalid!").into()); + } + n + } else { + XMLUuid::new_random() + }; + + if let Some(n) = &self.genid { + if !n.is_valid() { + return Err(StructureExtraction("VM genid is invalid!").into()); + } + } + + if let Some(n) = &self.title { + if n.contains('\n') { + return Err(StructureExtraction("VM title contain newline char!").into()); + } + } + + if self.memory < constants::MIN_VM_MEMORY || self.memory > constants::MAX_VM_MEMORY { + return Err(StructureExtraction("VM memory is invalid!").into()); + } + + if self.number_vcpu == 0 || (self.number_vcpu != 1 && self.number_vcpu.is_odd()) { + return Err(StructureExtraction("Invalid number of vCPU specified!").into()); + } + + let mut disks = vec![]; + + for iso_file in &self.iso_files { + if !files_utils::check_file_name(iso_file) { + return Err(StructureExtraction("ISO filename is invalid!").into()); + } + + let path = AppConfig::get().iso_storage_path().join(iso_file); + + if !path.exists() { + return Err(StructureExtraction("Specified ISO file does not exists!").into()); + } + + disks.push(DiskXML { + r#type: "file".to_string(), + device: "cdrom".to_string(), + driver: DiskDriverXML { + name: "qemu".to_string(), + r#type: "raw".to_string(), + cache: "none".to_string(), + }, + source: DiskSourceXML { + file: path.to_string_lossy().to_string(), + }, + target: DiskTargetXML { + dev: format!( + "hd{}", + ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"][disks.len()] + ), + bus: "sata".to_string(), + }, + readonly: Some(DiskReadOnlyXML {}), + boot: DiskBootXML { + order: (disks.len() + 1).to_string(), + }, + address: None, + }) + } + + let (vnc_graphics, vnc_video) = match self.vnc_access { + true => ( + Some(GraphicsXML { + r#type: "vnc".to_string(), + socket: AppConfig::get() + .vnc_socket_for_domain(&self.name) + .to_string_lossy() + .to_string(), + }), + Some(VideoXML { + model: VideoModelXML { + r#type: "virtio".to_string(), //"qxl".to_string(), + }, + }), + ), + false => (None, None), + }; + + // Check disks name for duplicates + for disk in &self.disks { + if self.disks.iter().filter(|d| d.name == disk.name).count() > 1 { + return Err(StructureExtraction("Two different disks have the same name!").into()); + } + } + + // Apply disks configuration + for disk in &self.disks { + disk.check_config()?; + disk.apply_config(uuid)?; + + if disk.delete { + continue; + } + + disks.push(DiskXML { + r#type: "file".to_string(), + device: "disk".to_string(), + driver: DiskDriverXML { + name: "qemu".to_string(), + r#type: "raw".to_string(), + cache: "none".to_string(), + }, + source: DiskSourceXML { + file: disk.disk_path(uuid).to_string_lossy().to_string(), + }, + target: DiskTargetXML { + dev: format!( + "vd{}", + ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"][disks.len()] + ), + bus: "virtio".to_string(), + }, + readonly: None, + boot: DiskBootXML { + order: (disks.len() + 1).to_string(), + }, + address: None, + }) + } + + let mut networks = vec![]; + for n in &self.networks { + networks.push(match &n.r#type { + NetworkType::UserspaceSLIRPStack => DomainNetInterfaceXML { + mac: NetMacAddress { + address: n.mac.to_string(), + }, + r#type: "user".to_string(), + source: None, + model: Some(NetIntModelXML { + r#type: "virtio".to_string(), + }), + }, + NetworkType::DefinedNetwork { network } => DomainNetInterfaceXML { + mac: NetMacAddress { + address: n.mac.to_string(), + }, + r#type: "network".to_string(), + source: Some(NetIntSourceXML { + network: network.to_string(), + }), + model: Some(NetIntModelXML { + r#type: "virtio".to_string(), + }), + }, + }) + } + + Ok(DomainXML { + r#type: "kvm".to_string(), + name: self.name.to_string(), + uuid: Some(uuid), + genid: self.genid.map(|i| i.0), + title: self.title.clone(), + description: self.description.clone(), + + os: OSXML { + r#type: OSTypeXML { + arch: match self.architecture { + VMArchitecture::I686 => "i686", + VMArchitecture::X86_64 => "x86_64", + } + .to_string(), + machine: "q35".to_string(), + body: "hvm".to_string(), + }, + firmware: "efi".to_string(), + loader: Some(OSLoaderXML { + secure: match self.boot_type { + BootType::UEFI => "no".to_string(), + BootType::UEFISecureBoot => "yes".to_string(), + }, + }), + }, + + features: FeaturesXML { acpi: ACPIXML {} }, + + devices: DevicesXML { + graphics: vnc_graphics, + video: vnc_video, + disks, + net_interfaces: networks, + inputs: vec![ + DomainInputXML { + r#type: "mouse".to_string(), + }, + DomainInputXML { + r#type: "keyboard".to_string(), + }, + DomainInputXML { + r#type: "tablet".to_string(), + }, + ], + tpm: match self.tpm_module { + true => Some(TPMDeviceXML { + model: "tpm-tis".to_string(), + backend: TPMBackendXML { + r#type: "emulator".to_string(), + version: "2.0".to_string(), + }, + }), + false => None, + }, + }, + + memory: DomainMemoryXML { + unit: "MB".to_string(), + memory: self.memory, + }, + + vcpu: DomainVCPUXML { + body: self.number_vcpu, + }, + + cpu: DomainCPUXML { + mode: "host-passthrough".to_string(), + topology: Some(DomainCPUTopology { + sockets: 1, + cores: match self.number_vcpu { + 1 => 1, + v => v / 2, + }, + threads: match self.number_vcpu { + 1 => 1, + _ => 2, + }, + }), + }, + + on_poweroff: "destroy".to_string(), + on_reboot: "restart".to_string(), + on_crash: "destroy".to_string(), + }) + } + + /// Turn a domain into a vm + pub fn from_domain(domain: DomainXML) -> anyhow::Result { + Ok(Self { + name: domain.name, + uuid: domain.uuid, + genid: domain.genid.map(XMLUuid), + title: domain.title, + description: domain.description, + boot_type: match domain.os.loader { + None => BootType::UEFI, + Some(l) => match l.secure.as_str() { + "yes" => BootType::UEFISecureBoot, + _ => BootType::UEFI, + }, + }, + architecture: match domain.os.r#type.arch.as_str() { + "i686" => VMArchitecture::I686, + "x86_64" => VMArchitecture::X86_64, + a => { + return Err(LibVirtStructError::DomainExtraction(format!( + "Unknown architecture: {a}! " + )) + .into()); + } + }, + number_vcpu: domain.vcpu.body, + memory: convert_size_unit_to_mb(&domain.memory.unit, domain.memory.memory)?, + vnc_access: domain.devices.graphics.is_some(), + iso_files: domain + .devices + .disks + .iter() + .filter(|d| d.device == "cdrom") + .map(|d| d.source.file.rsplit_once('/').unwrap().1.to_string()) + .collect(), + + disks: domain + .devices + .disks + .iter() + .filter(|d| d.device == "disk") + .map(|d| Disk::load_from_file(&d.source.file).unwrap()) + .collect(), + + networks: domain + .devices + .net_interfaces + .iter() + .map(|d| { + Ok(Network { + 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(), + }, + a => { + return Err(LibVirtStructError::DomainExtraction(format!( + "Unknown network interface type: {a}! " + ))); + } + }, + }) + }) + .collect::, _>>()?, + + tpm_module: domain.devices.tpm.is_some(), + }) + } +} diff --git a/virtweb_backend/src/utils/files_utils.rs b/virtweb_backend/src/utils/files_utils.rs index 54fc403..4d3f957 100644 --- a/virtweb_backend/src/utils/files_utils.rs +++ b/virtweb_backend/src/utils/files_utils.rs @@ -1,6 +1,13 @@ +use std::ops::{Div, Mul}; use std::os::unix::fs::PermissionsExt; use std::path::Path; +#[derive(thiserror::Error, Debug)] +enum FilesUtilsError { + #[error("UnitConvertError: {0}")] + UnitConvert(String), +} + const INVALID_CHARS: [&str; 19] = [ "@", "\\", "/", ":", ",", "<", ">", "%", "'", "\"", "?", "{", "}", "$", "*", "|", ";", "=", "\t", @@ -28,9 +35,31 @@ pub fn set_file_permission>(path: P, mode: u32) -> anyhow::Result Ok(()) } +/// Convert size unit to MB +pub fn convert_size_unit_to_mb(unit: &str, value: usize) -> anyhow::Result { + let fact = match unit { + "bytes" | "b" => 1f64, + "KB" => 1000f64, + "MB" => 1000f64 * 1000f64, + "GB" => 1000f64 * 1000f64 * 1000f64, + "TB" => 1000f64 * 1000f64 * 1000f64 * 1000f64, + + "k" | "KiB" => 1024f64, + "M" | "MiB" => 1024f64 * 1024f64, + "G" | "GiB" => 1024f64 * 1024f64 * 1024f64, + "T" | "TiB" => 1024f64 * 1024f64 * 1024f64 * 1024f64, + + _ => { + return Err(FilesUtilsError::UnitConvert(format!("Unknown size unit: {unit}")).into()); + } + }; + + Ok((value as f64).mul(fact.div((1000 * 1000) as f64)).ceil() as usize) +} + #[cfg(test)] mod test { - use crate::utils::files_utils::check_file_name; + use crate::utils::files_utils::{check_file_name, convert_size_unit_to_mb}; #[test] fn empty_file_name() { @@ -56,4 +85,14 @@ mod test { fn valid_file_name() { assert!(check_file_name("test.iso")); } + + #[test] + fn convert_units_mb() { + assert_eq!(convert_size_unit_to_mb("MB", 1).unwrap(), 1); + assert_eq!(convert_size_unit_to_mb("MB", 1000).unwrap(), 1000); + assert_eq!(convert_size_unit_to_mb("GB", 1000).unwrap(), 1000 * 1000); + assert_eq!(convert_size_unit_to_mb("GB", 1).unwrap(), 1000); + assert_eq!(convert_size_unit_to_mb("GiB", 3).unwrap(), 3222); + assert_eq!(convert_size_unit_to_mb("KiB", 488281).unwrap(), 500); + } } diff --git a/virtweb_backend/src/utils/mod.rs b/virtweb_backend/src/utils/mod.rs index 066b06d..f10b3e2 100644 --- a/virtweb_backend/src/utils/mod.rs +++ b/virtweb_backend/src/utils/mod.rs @@ -1,5 +1,6 @@ pub mod disks_utils; pub mod files_utils; +pub mod net_utils; pub mod rand_utils; pub mod time_utils; pub mod url_utils; diff --git a/virtweb_backend/src/utils/net_utils.rs b/virtweb_backend/src/utils/net_utils.rs new file mode 100644 index 0000000..b0bd80e --- /dev/null +++ b/virtweb_backend/src/utils/net_utils.rs @@ -0,0 +1,19 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +pub fn extract_ipv4(ip: IpAddr) -> Ipv4Addr { + match ip { + IpAddr::V4(i) => i, + IpAddr::V6(_) => { + panic!("IPv6 found in IPv4 definition!") + } + } +} + +pub fn extract_ipv6(ip: IpAddr) -> Ipv6Addr { + match ip { + IpAddr::V4(_) => { + panic!("IPv4 found in IPv6 definition!") + } + IpAddr::V6(i) => i, + } +}