Compare commits
	
		
			38 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 706bce0fd8 | |||
| ffac6991c4 | |||
| f890cba5a4 | |||
| e561942cf7 | |||
| 219fc184ee | |||
| 3407c068e1 | |||
| afe5db1fcd | |||
| 085deff4f7 | |||
| 0175726696 | |||
| a8046ebff8 | |||
| 767d2015df | |||
| d4ef389852 | |||
| 2b145ebeff | |||
| 06ddf57b5c | |||
| b4f65a6703 | |||
| d741e12653 | |||
| 9256b76495 | |||
| e638829da7 | |||
| 81f60ce766 | |||
| 388a1ed478 | |||
| c6c1ce26d3 | |||
| b3f89309c4 | |||
| 8182ecd7f6 | |||
| 7b74e7b75a | |||
| 61c567846d | |||
| 246f5ef842 | |||
| 9d4f19822d | |||
| f7777fe085 | |||
| 3849b0d51d | |||
| b4f765d486 | |||
| d8a6b58c52 | |||
| d053490a47 | |||
| 66dcf668f0 | |||
| af1e406945 | |||
| f49b947884 | |||
| 483acde546 | |||
| 3a7b2445a6 | |||
| cd55e6867e | 
							
								
								
									
										121
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										121
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,123 +1,8 @@ | ||||
| # VirtWEB | ||||
| WIP project | ||||
|  | ||||
| ## Development requirements | ||||
| 1. The `libvirt-dev` package must be installed: | ||||
|  | ||||
| ```bash | ||||
| sudo apt install libvirt-dev | ||||
| ``` | ||||
|  | ||||
| 2. Libvirt must also be installed: | ||||
| ```bash | ||||
| sudo apt install qemu-kvm libvirt-daemon-system | ||||
| ``` | ||||
|  | ||||
| 3. Allow the current user to manage VMs: | ||||
| ``` | ||||
| sudo adduser $USER libvirt | ||||
| sudo adduser $USER kvm  | ||||
| ``` | ||||
|  | ||||
| > Note: You will need to login again for this change to take effect. | ||||
|  | ||||
| ## Setup for dev | ||||
| Please refer to this guide: [virtweb_docs/SETUP_DEV.md](virtweb_docs/SETUP_DEV.md) | ||||
|  | ||||
| ## Production requirements | ||||
| ### TODO | ||||
| TODO | ||||
|  | ||||
| ### Manual port forwarding without a LibVirt HOOK | ||||
| * Allow ip forwarding in the kernel: edit `/etc/sysctl.conf` and uncomment the following line: | ||||
|  | ||||
| ``` | ||||
| net.ipv4.ip_forward=1 | ||||
| ``` | ||||
|  | ||||
| * To reload `sysctl` without reboot: | ||||
|  | ||||
| ``` | ||||
| sudo sysctl -p /etc/sysctl.conf | ||||
| ``` | ||||
|  | ||||
| * Create the following IPTables rules: | ||||
|  | ||||
| ``` | ||||
| UP_DEV=$(ip a | grep "192.168.1." -B 2 | head -n 1 | cut -d ':' -f 2 | | ||||
|  tr -d ' ') | ||||
| LOCAL_DEV=$(ip a | grep "192.168.25." -B 2 | head -n 1 | cut -d ':' -f 2 | tr -d ' ') | ||||
| echo "$UP_DEV -> $LOCAL_DEV" | ||||
|  | ||||
| GUEST_IP=192.168.25.189 | ||||
| HOST_PORT=8085 | ||||
| GUEST_PORT=8085 | ||||
|  | ||||
| # connections from outside | ||||
| sudo iptables -I FORWARD -o $LOCAL_DEV -d  $GUEST_IP -j ACCEPT | ||||
| sudo iptables -t nat -I PREROUTING -p tcp --dport $HOST_PORT -j DNAT --to $GUEST_IP:$GUEST_PORT | ||||
| ``` | ||||
|  | ||||
| * Theses rules can be persisted using `iptables-save` then, or using a libvirt hook. | ||||
|  | ||||
|  | ||||
| ### Manual port forwarding with a LibVirt HOOK | ||||
| * Allow ip forwarding in the kernel: edit `/etc/sysctl.conf` and uncomment the following line: | ||||
|  | ||||
| ``` | ||||
| net.ipv4.ip_forward=1 | ||||
| ``` | ||||
|  | ||||
| * To reload `sysctl` without reboot: | ||||
|  | ||||
| ``` | ||||
| sudo sysctl -p /etc/sysctl.conf | ||||
| ``` | ||||
|  | ||||
| * Get the following information, using the web ui or `virsh`: | ||||
| 	* The name of the target guest | ||||
| 	* The IP and port of the guest who will receive the connection | ||||
| 	* The port of the host that will be forwarded to the guest | ||||
|  | ||||
| * Stop the guest if its running, either using `virsh` or from the web ui | ||||
|  | ||||
| * Create or append the following content to the file `/etc/libvirt/hooks/qemu`: | ||||
|  | ||||
| ```bash | ||||
| #!/bin/bash | ||||
|  | ||||
| # IMPORTANT: Change the "VM NAME" string to match your actual VM Name. | ||||
| # In order to create rules to other VMs, just duplicate the below block and configure | ||||
| # it accordingly. | ||||
| if [ "${1}" = "VM NAME" ]; then | ||||
|  | ||||
|  # Update the following variables to fit your setup | ||||
|  GUEST_IP= | ||||
|  GUEST_PORT= | ||||
|  HOST_PORT= | ||||
|  | ||||
|  if [ "${2}" = "stopped" ] || [ "${2}" = "reconnect" ]; then | ||||
|   /sbin/iptables -D FORWARD -o virbr0 -p tcp -d $GUEST_IP --dport $GUEST_PORT -j ACCEPT | ||||
|   /sbin/iptables -t nat -D PREROUTING -p tcp --dport $HOST_PORT -j DNAT --to $GUEST_IP:$GUEST_PORT | ||||
|  fi | ||||
|  if [ "${2}" = "start" ] || [ "${2}" = "reconnect" ]; then | ||||
|   /sbin/iptables -I FORWARD -o virbr0 -p tcp -d $GUEST_IP --dport $GUEST_PORT -j ACCEPT | ||||
|   /sbin/iptables -t nat -I PREROUTING -p tcp --dport $HOST_PORT -j DNAT --to $GUEST_IP:$GUEST_PORT | ||||
|  fi | ||||
| fi | ||||
| ``` | ||||
|  | ||||
| * Make the hook executable: | ||||
|  | ||||
| ```bash | ||||
| sudo chmod +x /etc/libvirt/hooks/qemu | ||||
| ``` | ||||
|  | ||||
| * Restart the `libvirtd` service: | ||||
|  | ||||
| ```bash | ||||
| sudo systemctl restart libvirtd.service | ||||
| ``` | ||||
|  | ||||
| * Start the guest | ||||
|  | ||||
|  | ||||
| > Note: this guide is based on https://wiki.libvirt.org/Networking.html | ||||
| Please refer to this guide: [virtweb_docs/SETUP_PROD.md](virtweb_docs/SETUP_PROD.md) | ||||
							
								
								
									
										11
									
								
								virtweb_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										11
									
								
								virtweb_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -1882,6 +1882,16 @@ dependencies = [ | ||||
|  "bytemuck", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "quick-xml" | ||||
| version = "0.31.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" | ||||
| dependencies = [ | ||||
|  "memchr", | ||||
|  "serde", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "quote" | ||||
| version = "1.0.33" | ||||
| @@ -2662,6 +2672,7 @@ dependencies = [ | ||||
|  "log", | ||||
|  "mime_guess", | ||||
|  "num", | ||||
|  "quick-xml", | ||||
|  "rand", | ||||
|  "reqwest", | ||||
|  "rust-embed", | ||||
|   | ||||
| @@ -23,6 +23,7 @@ actix-http = "3.4.0" | ||||
| serde = { version = "1.0.193", features = ["derive"] } | ||||
| serde_json = "1.0.108" | ||||
| serde-xml-rs = "0.6.0" | ||||
| quick-xml = { version = "0.31.0", features = ["serialize", "overlapped-lists"] } | ||||
| futures-util = "0.3.28" | ||||
| anyhow = "1.0.75" | ||||
| actix-multipart = "0.6.1" | ||||
| @@ -43,4 +44,4 @@ ipnetwork = "0.20.0" | ||||
| num = "0.4.1" | ||||
| rust-embed = { version = "8.1.0" } | ||||
| mime_guess = "2.0.4" | ||||
| dotenvy = "0.15.7" | ||||
| dotenvy = "0.15.7" | ||||
|   | ||||
| @@ -1,12 +1,19 @@ | ||||
| use crate::app_config::AppConfig; | ||||
| use crate::libvirt_lib_structures::{DomainState, DomainXML, 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::nw_filter::{NetworkFilter, NetworkFilterName}; | ||||
| use crate::libvirt_rest_structures::vm::*; | ||||
| use actix::{Actor, Context, Handler, Message}; | ||||
| use image::ImageOutputFormat; | ||||
| use std::io::Cursor; | ||||
| use virt::connect::Connect; | ||||
| use virt::domain::Domain; | ||||
| use virt::network::Network; | ||||
| use virt::nwfilter::NWFilter; | ||||
| use virt::stream::Stream; | ||||
| use virt::sys; | ||||
| use virt::sys::VIR_DOMAIN_XML_SECURE; | ||||
| @@ -95,7 +102,7 @@ impl Handler<GetDomainXMLReq> for LibVirtActor { | ||||
|         let domain = Domain::lookup_by_uuid_string(&self.m, &msg.0.as_string())?; | ||||
|         let xml = domain.get_xml_desc(VIR_DOMAIN_XML_SECURE)?; | ||||
|         log::debug!("XML = {}", xml); | ||||
|         Ok(serde_xml_rs::from_str(&xml)?) | ||||
|         DomainXML::parse_xml(&xml) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -115,17 +122,24 @@ impl Handler<GetSourceDomainXMLReq> for LibVirtActor { | ||||
|  | ||||
| #[derive(Message)] | ||||
| #[rtype(result = "anyhow::Result<XMLUuid>")] | ||||
| pub struct DefineDomainReq(pub DomainXML); | ||||
| pub struct DefineDomainReq(pub VMInfo, pub DomainXML); | ||||
|  | ||||
| impl Handler<DefineDomainReq> for LibVirtActor { | ||||
|     type Result = anyhow::Result<XMLUuid>; | ||||
|  | ||||
|     fn handle(&mut self, msg: DefineDomainReq, _ctx: &mut Self::Context) -> Self::Result { | ||||
|         let xml = msg.0.into_xml()?; | ||||
|     fn handle(&mut self, mut msg: DefineDomainReq, _ctx: &mut Self::Context) -> Self::Result { | ||||
|         let xml = msg.1.as_xml()?; | ||||
|  | ||||
|         log::debug!("Define domain:\n{}", xml); | ||||
|         let domain = Domain::define_xml(&self.m, &xml)?; | ||||
|         XMLUuid::parse_from_str(&domain.get_uuid_string()?) | ||||
|         let uuid = XMLUuid::parse_from_str(&domain.get_uuid_string()?)?; | ||||
|  | ||||
|         // Save a copy of the source definition | ||||
|         msg.0.uuid = Some(uuid); | ||||
|         let json = serde_json::to_string(&msg.0)?; | ||||
|         std::fs::write(AppConfig::get().vm_definition_path(&msg.0.name), json)?; | ||||
|  | ||||
|         Ok(uuid) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -155,6 +169,12 @@ impl Handler<DeleteDomainReq> for LibVirtActor { | ||||
|             std::fs::remove_file(vnc_socket)?; | ||||
|         } | ||||
|  | ||||
|         // Remove backup definition | ||||
|         let backup_definition = AppConfig::get().vm_definition_path(&domain_name); | ||||
|         if backup_definition.exists() { | ||||
|             std::fs::remove_file(backup_definition)?; | ||||
|         } | ||||
|  | ||||
|         // Delete the domain | ||||
|         domain.undefine_flags(match msg.keep_files { | ||||
|             true => sys::VIR_DOMAIN_UNDEFINE_KEEP_NVRAM, | ||||
| @@ -360,20 +380,27 @@ impl Handler<SetDomainAutostart> for LibVirtActor { | ||||
|  | ||||
| #[derive(Message)] | ||||
| #[rtype(result = "anyhow::Result<XMLUuid>")] | ||||
| pub struct DefineNetwork(pub NetworkXML); | ||||
| pub struct DefineNetwork(pub NetworkInfo, pub NetworkXML); | ||||
|  | ||||
| impl Handler<DefineNetwork> for LibVirtActor { | ||||
|     type Result = anyhow::Result<XMLUuid>; | ||||
|  | ||||
|     fn handle(&mut self, msg: DefineNetwork, _ctx: &mut Self::Context) -> Self::Result { | ||||
|         log::debug!("Define network: {:?}", msg.0); | ||||
|     fn handle(&mut self, mut msg: DefineNetwork, _ctx: &mut Self::Context) -> Self::Result { | ||||
|         log::debug!("Define network: {:?}", msg.1); | ||||
|  | ||||
|         log::debug!("Source network structure: {:#?}", msg.0); | ||||
|         let network_xml = msg.0.into_xml()?; | ||||
|         log::debug!("Source network structure: {:#?}", msg.1); | ||||
|         let network_xml = msg.1.as_xml()?; | ||||
|         log::debug!("Define network XML: {network_xml}"); | ||||
|  | ||||
|         let network = Network::define_xml(&self.m, &network_xml)?; | ||||
|         XMLUuid::parse_from_str(&network.get_uuid_string()?) | ||||
|         let uuid = XMLUuid::parse_from_str(&network.get_uuid_string()?)?; | ||||
|  | ||||
|         // Save a copy of the source definition | ||||
|         msg.0.uuid = Some(uuid); | ||||
|         let json = serde_json::to_string(&msg.0)?; | ||||
|         std::fs::write(AppConfig::get().net_definition_path(&msg.0.name), json)?; | ||||
|  | ||||
|         Ok(uuid) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -409,7 +436,7 @@ impl Handler<GetNetworkXMLReq> for LibVirtActor { | ||||
|         let network = Network::lookup_by_uuid_string(&self.m, &msg.0.as_string())?; | ||||
|         let xml = network.get_xml_desc(0)?; | ||||
|         log::debug!("XML = {}", xml); | ||||
|         Ok(serde_xml_rs::from_str(&xml)?) | ||||
|         NetworkXML::parse_xml(&xml) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -437,7 +464,15 @@ impl Handler<DeleteNetwork> for LibVirtActor { | ||||
|     fn handle(&mut self, msg: DeleteNetwork, _ctx: &mut Self::Context) -> Self::Result { | ||||
|         log::debug!("Delete network: {}\n", msg.0.as_string()); | ||||
|         let network = Network::lookup_by_uuid_string(&self.m, &msg.0.as_string())?; | ||||
|         let network_name = network.get_name()?; | ||||
|         network.undefine()?; | ||||
|  | ||||
|         // Remove backup definition | ||||
|         let backup_definition = AppConfig::get().net_definition_path(&network_name); | ||||
|         if backup_definition.exists() { | ||||
|             std::fs::remove_file(backup_definition)?; | ||||
|         } | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| @@ -521,3 +556,107 @@ impl Handler<StopNetwork> for LibVirtActor { | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Message)] | ||||
| #[rtype(result = "anyhow::Result<Vec<XMLUuid>>")] | ||||
| pub struct GetNWFiltersListReq; | ||||
|  | ||||
| impl Handler<GetNWFiltersListReq> for LibVirtActor { | ||||
|     type Result = anyhow::Result<Vec<XMLUuid>>; | ||||
|  | ||||
|     fn handle(&mut self, _msg: GetNWFiltersListReq, _ctx: &mut Self::Context) -> Self::Result { | ||||
|         log::debug!("Get full list of network filters"); | ||||
|         let networks = self.m.list_all_nw_filters(0)?; | ||||
|         let mut ids = Vec::with_capacity(networks.len()); | ||||
|  | ||||
|         for d in networks { | ||||
|             ids.push(XMLUuid::parse_from_str(&d.get_uuid_string()?)?); | ||||
|         } | ||||
|  | ||||
|         Ok(ids) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Message)] | ||||
| #[rtype(result = "anyhow::Result<NetworkFilterXML>")] | ||||
| pub struct GetNWFilterXMLReq(pub XMLUuid); | ||||
|  | ||||
| impl Handler<GetNWFilterXMLReq> for LibVirtActor { | ||||
|     type Result = anyhow::Result<NetworkFilterXML>; | ||||
|  | ||||
|     fn handle(&mut self, msg: GetNWFilterXMLReq, _ctx: &mut Self::Context) -> Self::Result { | ||||
|         log::debug!("Get network filter XML:\n{}", msg.0.as_string()); | ||||
|         let filter = NWFilter::lookup_by_uuid_string(&self.m, &msg.0.as_string())?; | ||||
|         let xml = filter.get_xml_desc(0)?; | ||||
|         log::debug!("XML = {}", xml); | ||||
|         NetworkFilterXML::parse_xml(xml) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Message)] | ||||
| #[rtype(result = "anyhow::Result<XMLUuid>")] | ||||
| pub struct DefineNWFilterReq(pub NetworkFilter, pub NetworkFilterXML); | ||||
|  | ||||
| impl Handler<DefineNWFilterReq> for LibVirtActor { | ||||
|     type Result = anyhow::Result<XMLUuid>; | ||||
|  | ||||
|     fn handle(&mut self, mut msg: DefineNWFilterReq, _ctx: &mut Self::Context) -> Self::Result { | ||||
|         let xml = msg.1.into_xml()?; | ||||
|  | ||||
|         log::debug!("Define network filter:\n{}", xml); | ||||
|         let filter = NWFilter::define_xml(&self.m, &xml)?; | ||||
|         let uuid = XMLUuid::parse_from_str(&filter.get_uuid_string()?)?; | ||||
|  | ||||
|         // Save a copy of the source definition | ||||
|         msg.0.uuid = Some(uuid); | ||||
|         let json = serde_json::to_string(&msg.0)?; | ||||
|         std::fs::write( | ||||
|             AppConfig::get().net_filter_definition_path(&msg.0.name), | ||||
|             json, | ||||
|         )?; | ||||
|  | ||||
|         Ok(uuid) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Message)] | ||||
| #[rtype(result = "anyhow::Result<()>")] | ||||
| pub struct DeleteNetworkFilter(pub XMLUuid); | ||||
|  | ||||
| impl Handler<DeleteNetworkFilter> for LibVirtActor { | ||||
|     type Result = anyhow::Result<()>; | ||||
|  | ||||
|     fn handle(&mut self, msg: DeleteNetworkFilter, _ctx: &mut Self::Context) -> Self::Result { | ||||
|         log::debug!("Delete network filter: {}\n", msg.0.as_string()); | ||||
|         let nw_filter = NWFilter::lookup_by_uuid_string(&self.m, &msg.0.as_string())?; | ||||
|         let nw_filter_name = nw_filter.get_name()?; | ||||
|         nw_filter.undefine()?; | ||||
|  | ||||
|         // Remove backup definition | ||||
|         let backup_definition = | ||||
|             AppConfig::get().net_filter_definition_path(&NetworkFilterName(nw_filter_name)); | ||||
|         if backup_definition.exists() { | ||||
|             std::fs::remove_file(backup_definition)?; | ||||
|         } | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Message)] | ||||
| #[rtype(result = "anyhow::Result<String>")] | ||||
| pub struct GetSourceNetworkFilterXMLReq(pub XMLUuid); | ||||
|  | ||||
| impl Handler<GetSourceNetworkFilterXMLReq> for LibVirtActor { | ||||
|     type Result = anyhow::Result<String>; | ||||
|  | ||||
|     fn handle( | ||||
|         &mut self, | ||||
|         msg: GetSourceNetworkFilterXMLReq, | ||||
|         _ctx: &mut Self::Context, | ||||
|     ) -> Self::Result { | ||||
|         log::debug!("Get nw filter source XML:\n{}", msg.0.as_string()); | ||||
|         let nwfilter = NWFilter::lookup_by_uuid_string(&self.m, &msg.0.as_string())?; | ||||
|         Ok(nwfilter.get_xml_desc(0)?) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| use crate::libvirt_lib_structures::XMLUuid; | ||||
| use crate::libvirt_rest_structures::nw_filter::NetworkFilterName; | ||||
| use clap::Parser; | ||||
| use std::net::IpAddr; | ||||
| use std::path::{Path, PathBuf}; | ||||
| @@ -134,14 +135,19 @@ impl AppConfig { | ||||
|  | ||||
|     /// Get auth cookie domain | ||||
|     pub fn cookie_domain(&self) -> Option<String> { | ||||
|         let domain = self.website_origin.split_once("://")?.1; | ||||
|         Some( | ||||
|             domain | ||||
|                 .split_once(':') | ||||
|                 .map(|s| s.0) | ||||
|                 .unwrap_or(domain) | ||||
|                 .to_string(), | ||||
|         ) | ||||
|         if cfg!(debug_assertions) { | ||||
|             let domain = self.website_origin.split_once("://")?.1; | ||||
|             Some( | ||||
|                 domain | ||||
|                     .split_once(':') | ||||
|                     .map(|s| s.0) | ||||
|                     .unwrap_or(domain) | ||||
|                     .to_string(), | ||||
|             ) | ||||
|         } else { | ||||
|             // In release mode, the web app is hosted on the same origin as the API | ||||
|             None | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Get app secret | ||||
| @@ -235,6 +241,23 @@ impl AppConfig { | ||||
|     pub fn vm_storage_path(&self, id: XMLUuid) -> PathBuf { | ||||
|         self.disks_storage_path().join(id.as_string()) | ||||
|     } | ||||
|  | ||||
|     pub fn definitions_path(&self) -> PathBuf { | ||||
|         self.storage_path().join("definitions") | ||||
|     } | ||||
|  | ||||
|     pub fn vm_definition_path(&self, name: &str) -> PathBuf { | ||||
|         self.definitions_path().join(format!("vm-{name}.json")) | ||||
|     } | ||||
|  | ||||
|     pub fn net_definition_path(&self, name: &str) -> PathBuf { | ||||
|         self.definitions_path().join(format!("net-{name}.json")) | ||||
|     } | ||||
|  | ||||
|     pub fn net_filter_definition_path(&self, name: &NetworkFilterName) -> PathBuf { | ||||
|         self.definitions_path() | ||||
|             .join(format!("nwfilter-{}.json", name.0)) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize)] | ||||
|   | ||||
| @@ -46,3 +46,31 @@ pub const DISK_SIZE_MAX: usize = 1000 * 1000 * 2; | ||||
|  | ||||
| /// Network mac address default prefix | ||||
| pub const NET_MAC_ADDR_PREFIX: &str = "52:54:00"; | ||||
|  | ||||
| /// Built-in network filter rules | ||||
| pub const BUILTIN_NETWORK_FILTER_RULES: [&str; 24] = [ | ||||
|     "allow-arp", | ||||
|     "allow-dhcp", | ||||
|     "allow-dhcp-server", | ||||
|     "allow-dhcpv6", | ||||
|     "allow-dhcpv6-server", | ||||
|     "allow-incoming-ipv4", | ||||
|     "allow-incoming-ipv6", | ||||
|     "allow-ipv4", | ||||
|     "allow-ipv6", | ||||
|     "clean-traffic", | ||||
|     "clean-traffic-gateway", | ||||
|     "no-arp-ip-spoofing", | ||||
|     "no-arp-mac-spoofing", | ||||
|     "no-arp-spoofing", | ||||
|     "no-ip-multicast", | ||||
|     "no-ip-spoofing", | ||||
|     "no-ipv6-multicast", | ||||
|     "no-ipv6-spoofing", | ||||
|     "no-mac-broadcast", | ||||
|     "no-mac-spoofing", | ||||
|     "no-other-l2-traffic", | ||||
|     "no-other-rarp-traffic", | ||||
|     "qemu-announce-self", | ||||
|     "qemu-announce-self-rarp", | ||||
| ]; | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| use crate::libvirt_client::LibVirtClient; | ||||
| use actix_http::StatusCode; | ||||
| use actix_web::body::BoxBody; | ||||
| use actix_web::{web, HttpResponse}; | ||||
| use std::error::Error; | ||||
| @@ -8,6 +9,7 @@ use std::io::ErrorKind; | ||||
| pub mod auth_controller; | ||||
| pub mod iso_controller; | ||||
| pub mod network_controller; | ||||
| pub mod nwfilter_controller; | ||||
| pub mod server_controller; | ||||
| pub mod static_controller; | ||||
| pub mod vm_controller; | ||||
| @@ -31,8 +33,15 @@ impl Display for HttpErr { | ||||
| } | ||||
|  | ||||
| impl actix_web::error::ResponseError for HttpErr { | ||||
|     fn status_code(&self) -> StatusCode { | ||||
|         match self { | ||||
|             HttpErr::Err(_) => StatusCode::INTERNAL_SERVER_ERROR, | ||||
|             HttpErr::HTTPResponse(r) => r.status(), | ||||
|         } | ||||
|     } | ||||
|     fn error_response(&self) -> HttpResponse<BoxBody> { | ||||
|         log::error!("Error while processing request! {}", self); | ||||
|  | ||||
|         HttpResponse::InternalServerError().body("Failed to execute request!") | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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)] | ||||
| @@ -10,7 +10,7 @@ pub struct NetworkID { | ||||
|  | ||||
| /// Create a new network | ||||
| pub async fn create(client: LibVirtReq, req: web::Json<NetworkInfo>) -> HttpResult { | ||||
|     let network = match req.0.to_virt_network() { | ||||
|     let network = match req.0.as_virt_network() { | ||||
|         Ok(d) => d, | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to extract network info! {e}"); | ||||
| @@ -20,7 +20,7 @@ pub async fn create(client: LibVirtReq, req: web::Json<NetworkInfo>) -> HttpResu | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     let uid = match client.update_network(network).await { | ||||
|     let uid = match client.update_network(req.0, network).await { | ||||
|         Ok(u) => u, | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to update network! {e}"); | ||||
| @@ -71,7 +71,7 @@ pub async fn update( | ||||
|     path: web::Path<NetworkID>, | ||||
|     body: web::Json<NetworkInfo>, | ||||
| ) -> HttpResult { | ||||
|     let mut network = match body.0.to_virt_network() { | ||||
|     let mut network = match body.0.as_virt_network() { | ||||
|         Ok(n) => n, | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to extract network info! {e}"); | ||||
| @@ -82,7 +82,7 @@ pub async fn update( | ||||
|     }; | ||||
|     network.uuid = Some(path.uid); | ||||
|  | ||||
|     if let Err(e) = client.update_network(network).await { | ||||
|     if let Err(e) = client.update_network(body.0, network).await { | ||||
|         log::error!("Failed to update network! {e}"); | ||||
|         return Ok( | ||||
|             HttpResponse::InternalServerError().json(format!("Failed to update network!\n${e}")) | ||||
|   | ||||
							
								
								
									
										113
									
								
								virtweb_backend/src/controllers/nwfilter_controller.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								virtweb_backend/src/controllers/nwfilter_controller.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | ||||
| use crate::constants; | ||||
| use crate::controllers::{HttpResult, LibVirtReq}; | ||||
| use crate::libvirt_lib_structures::XMLUuid; | ||||
| use crate::libvirt_rest_structures::nw_filter::NetworkFilter; | ||||
| use actix_web::{web, HttpResponse}; | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| pub struct NetworkFilterID { | ||||
|     uid: XMLUuid, | ||||
| } | ||||
|  | ||||
| /// Create a new nw filter | ||||
| pub async fn create(client: LibVirtReq, req: web::Json<NetworkFilter>) -> HttpResult { | ||||
|     let network = match req.0.rest2lib() { | ||||
|         Ok(d) => d, | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to extract network filter info! {e}"); | ||||
|             return Ok(HttpResponse::BadRequest() | ||||
|                 .json(format!("Failed to extract network filter info! {e}"))); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     if constants::BUILTIN_NETWORK_FILTER_RULES.contains(&network.name.as_str()) { | ||||
|         return Ok(HttpResponse::ExpectationFailed() | ||||
|             .json("Builtin network filter rules shall not be modified!")); | ||||
|     } | ||||
|  | ||||
|     let uid = match client.update_network_filter(req.0, network).await { | ||||
|         Ok(u) => u, | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to update network filter! {e}"); | ||||
|             return Ok(HttpResponse::InternalServerError() | ||||
|                 .json(format!("Failed to update network filter! {e}"))); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     Ok(HttpResponse::Ok().json(NetworkFilterID { uid })) | ||||
| } | ||||
|  | ||||
| /// Get the list of network filters | ||||
| pub async fn list(client: LibVirtReq) -> HttpResult { | ||||
|     let networks = match client.get_full_network_filters_list().await { | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to get the list of network filters! {e}"); | ||||
|             return Ok(HttpResponse::InternalServerError() | ||||
|                 .json(format!("Failed to get the list of networks! {e}"))); | ||||
|         } | ||||
|         Ok(l) => l, | ||||
|     }; | ||||
|  | ||||
|     let networks = networks | ||||
|         .into_iter() | ||||
|         .map(|n| NetworkFilter::lib2rest(n).unwrap()) | ||||
|         .collect::<Vec<_>>(); | ||||
|  | ||||
|     Ok(HttpResponse::Ok().json(networks)) | ||||
| } | ||||
|  | ||||
| /// Get the information about a single network filter | ||||
| pub async fn get_single(client: LibVirtReq, req: web::Path<NetworkFilterID>) -> HttpResult { | ||||
|     let nwfilter = NetworkFilter::lib2rest(client.get_single_network_filter(req.uid).await?)?; | ||||
|     Ok(HttpResponse::Ok().json(nwfilter)) | ||||
| } | ||||
|  | ||||
| /// Get the XML source description of a single network filter | ||||
| pub async fn single_src(client: LibVirtReq, req: web::Path<NetworkFilterID>) -> HttpResult { | ||||
|     let xml = client.get_single_network_filter_xml(req.uid).await?; | ||||
|     Ok(HttpResponse::Ok().content_type("application/xml").body(xml)) | ||||
| } | ||||
|  | ||||
| /// Update the information about a single network filter | ||||
| pub async fn update( | ||||
|     client: LibVirtReq, | ||||
|     path: web::Path<NetworkFilterID>, | ||||
|     body: web::Json<NetworkFilter>, | ||||
| ) -> HttpResult { | ||||
|     let mut network = match body.0.rest2lib() { | ||||
|         Ok(n) => n, | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to extract network filter info! {e}"); | ||||
|             return Ok(HttpResponse::BadRequest() | ||||
|                 .json(format!("Failed to extract network filter info!\n${e}"))); | ||||
|         } | ||||
|     }; | ||||
|     network.uuid = Some(path.uid); | ||||
|  | ||||
|     if constants::BUILTIN_NETWORK_FILTER_RULES.contains(&network.name.as_str()) { | ||||
|         return Ok(HttpResponse::ExpectationFailed() | ||||
|             .json("Builtin network filter rules shall not be modified!")); | ||||
|     } | ||||
|  | ||||
|     if let Err(e) = client.update_network_filter(body.0, network).await { | ||||
|         log::error!("Failed to update network filter! {e}"); | ||||
|         return Ok(HttpResponse::InternalServerError() | ||||
|             .json(format!("Failed to update network filter!\n${e}"))); | ||||
|     } | ||||
|  | ||||
|     Ok(HttpResponse::Ok().json("Network filter updated")) | ||||
| } | ||||
|  | ||||
| /// Delete a network filter | ||||
| pub async fn delete(client: LibVirtReq, path: web::Path<NetworkFilterID>) -> HttpResult { | ||||
|     // Prevent deletion of default rules | ||||
|     let network = client.get_single_network_filter(path.uid).await?; | ||||
|     if constants::BUILTIN_NETWORK_FILTER_RULES.contains(&network.name.as_str()) { | ||||
|         return Ok(HttpResponse::ExpectationFailed() | ||||
|             .json("Builtin network filter rules shall not be deleted!")); | ||||
|     } | ||||
|  | ||||
|     client.delete_network_filter(path.uid).await?; | ||||
|  | ||||
|     Ok(HttpResponse::Ok().json("Network deleted")) | ||||
| } | ||||
| @@ -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}; | ||||
|  | ||||
| @@ -16,6 +16,7 @@ struct StaticConfig { | ||||
|     iso_mimetypes: &'static [&'static str], | ||||
|     net_mac_prefix: &'static str, | ||||
|     constraints: ServerConstraints, | ||||
|     builtin_network_rules: &'static [&'static str], | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize)] | ||||
| @@ -45,6 +46,7 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder { | ||||
|         oidc_auth_enabled: !AppConfig::get().disable_oidc, | ||||
|         iso_mimetypes: &constants::ALLOWED_ISO_MIME_TYPES, | ||||
|         net_mac_prefix: constants::NET_MAC_ADDR_PREFIX, | ||||
|         builtin_network_rules: &constants::BUILTIN_NETWORK_FILTER_RULES, | ||||
|         constraints: ServerConstraints { | ||||
|             iso_max_size: constants::ISO_MAX_SIZE, | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
|  | ||||
| @@ -20,7 +21,7 @@ struct VMUuid { | ||||
|  | ||||
| /// Create a new VM | ||||
| pub async fn create(client: LibVirtReq, req: web::Json<VMInfo>) -> HttpResult { | ||||
|     let domain = match req.0.to_domain() { | ||||
|     let domain = match req.0.as_tomain() { | ||||
|         Ok(d) => d, | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to extract domain info! {e}"); | ||||
| @@ -29,7 +30,7 @@ pub async fn create(client: LibVirtReq, req: web::Json<VMInfo>) -> HttpResult { | ||||
|             ); | ||||
|         } | ||||
|     }; | ||||
|     let id = match client.update_domain(domain).await { | ||||
|     let id = match client.update_domain(req.0, domain).await { | ||||
|         Ok(i) => i, | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to update domain info! {e}"); | ||||
| @@ -111,13 +112,18 @@ pub async fn update( | ||||
|     id: web::Path<SingleVMUUidReq>, | ||||
|     req: web::Json<VMInfo>, | ||||
| ) -> HttpResult { | ||||
|     let mut domain = req.0.to_domain().map_err(|e| { | ||||
|         log::error!("Failed to extract domain info! {e}"); | ||||
|         HttpResponse::BadRequest().json(format!("Failed to extract domain info! {e}")) | ||||
|     })?; | ||||
|     let mut domain = match req.0.as_tomain() { | ||||
|         Ok(d) => d, | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to extract domain info! {e}"); | ||||
|             return Ok( | ||||
|                 HttpResponse::BadRequest().json(format!("Failed to extract domain info! {e}")) | ||||
|             ); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     domain.uuid = Some(id.uid); | ||||
|     if let Err(e) = client.update_domain(domain).await { | ||||
|     if let Err(e) = client.update_domain(req.0, domain).await { | ||||
|         log::error!("Failed to update domain info! {e}"); | ||||
|         return Ok(HttpResponse::BadRequest().json(format!("Failed to update domain info!\n{e}"))); | ||||
|     } | ||||
|   | ||||
| @@ -1,7 +1,13 @@ | ||||
| use crate::actors::libvirt_actor; | ||||
| use crate::actors::libvirt_actor::LibVirtActor; | ||||
| use crate::libvirt_lib_structures::{DomainState, DomainXML, NetworkXML, XMLUuid}; | ||||
| use crate::libvirt_rest_structures::HypervisorInfo; | ||||
| 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::nw_filter::NetworkFilter; | ||||
| use crate::libvirt_rest_structures::vm::VMInfo; | ||||
| use actix::Addr; | ||||
|  | ||||
| #[derive(Clone)] | ||||
| @@ -36,8 +42,10 @@ impl LibVirtClient { | ||||
|     } | ||||
|  | ||||
|     /// Update a domain | ||||
|     pub async fn update_domain(&self, xml: DomainXML) -> anyhow::Result<XMLUuid> { | ||||
|         self.0.send(libvirt_actor::DefineDomainReq(xml)).await? | ||||
|     pub async fn update_domain(&self, vm_def: VMInfo, xml: DomainXML) -> anyhow::Result<XMLUuid> { | ||||
|         self.0 | ||||
|             .send(libvirt_actor::DefineDomainReq(vm_def, xml)) | ||||
|             .await? | ||||
|     } | ||||
|  | ||||
|     /// Delete a domain | ||||
| @@ -100,8 +108,14 @@ impl LibVirtClient { | ||||
|     } | ||||
|  | ||||
|     /// Update a network configuration | ||||
|     pub async fn update_network(&self, network: NetworkXML) -> anyhow::Result<XMLUuid> { | ||||
|         self.0.send(libvirt_actor::DefineNetwork(network)).await? | ||||
|     pub async fn update_network( | ||||
|         &self, | ||||
|         net_def: NetworkInfo, | ||||
|         network: NetworkXML, | ||||
|     ) -> anyhow::Result<XMLUuid> { | ||||
|         self.0 | ||||
|             .send(libvirt_actor::DefineNetwork(net_def, network)) | ||||
|             .await? | ||||
|     } | ||||
|  | ||||
|     /// Get the full list of networks | ||||
| @@ -157,4 +171,42 @@ impl LibVirtClient { | ||||
|     pub async fn stop_network(&self, id: XMLUuid) -> anyhow::Result<()> { | ||||
|         self.0.send(libvirt_actor::StopNetwork(id)).await? | ||||
|     } | ||||
|  | ||||
|     /// Get the full list of network filters | ||||
|     pub async fn get_full_network_filters_list(&self) -> anyhow::Result<Vec<NetworkFilterXML>> { | ||||
|         let ids = self.0.send(libvirt_actor::GetNWFiltersListReq).await??; | ||||
|         let mut info = Vec::with_capacity(ids.len()); | ||||
|         for id in ids { | ||||
|             info.push(self.get_single_network_filter(id).await?) | ||||
|         } | ||||
|         Ok(info) | ||||
|     } | ||||
|  | ||||
|     /// Get the information about a single domain | ||||
|     pub async fn get_single_network_filter(&self, id: XMLUuid) -> anyhow::Result<NetworkFilterXML> { | ||||
|         self.0.send(libvirt_actor::GetNWFilterXMLReq(id)).await? | ||||
|     } | ||||
|  | ||||
|     /// Get the source XML configuration of a single network filter | ||||
|     pub async fn get_single_network_filter_xml(&self, id: XMLUuid) -> anyhow::Result<String> { | ||||
|         self.0 | ||||
|             .send(libvirt_actor::GetSourceNetworkFilterXMLReq(id)) | ||||
|             .await? | ||||
|     } | ||||
|  | ||||
|     /// Update the information about a single domain | ||||
|     pub async fn update_network_filter( | ||||
|         &self, | ||||
|         nwf_def: NetworkFilter, | ||||
|         xml: NetworkFilterXML, | ||||
|     ) -> anyhow::Result<XMLUuid> { | ||||
|         self.0 | ||||
|             .send(libvirt_actor::DefineNWFilterReq(nwf_def, xml)) | ||||
|             .await? | ||||
|     } | ||||
|  | ||||
|     /// Delete a network filter | ||||
|     pub async fn delete_network_filter(&self, id: XMLUuid) -> anyhow::Result<()> { | ||||
|         self.0.send(libvirt_actor::DeleteNetworkFilter(id)).await? | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,540 +0,0 @@ | ||||
| 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<Self> { | ||||
|         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<OSLoaderXML>, | ||||
| } | ||||
|  | ||||
| /// 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 = "interface")] | ||||
| pub struct DomainNetInterfaceXML { | ||||
|     #[serde(rename(serialize = "@type"))] | ||||
|     pub r#type: String, | ||||
|     pub mac: NetMacAddress, | ||||
|     pub source: Option<NetIntSourceXML>, | ||||
| } | ||||
|  | ||||
| #[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<GraphicsXML>, | ||||
|  | ||||
|     /// Graphics (used for VNC) | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub video: Option<VideoXML>, | ||||
|  | ||||
|     /// Disks (used for storage) | ||||
|     #[serde(default, rename = "disk", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub disks: Vec<DiskXML>, | ||||
|  | ||||
|     /// Networks cards | ||||
|     #[serde(default, rename = "interface", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub net_interfaces: Vec<DomainNetInterfaceXML>, | ||||
|  | ||||
|     /// Input devices | ||||
|     #[serde(default, rename = "input", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub inputs: Vec<DomainInputXML>, | ||||
|  | ||||
|     /// TPM device | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub tpm: Option<TPMDeviceXML>, | ||||
| } | ||||
|  | ||||
| /// 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<DiskReadOnlyXML>, | ||||
|     pub boot: DiskBootXML, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub address: Option<DiskAddressXML>, | ||||
| } | ||||
|  | ||||
| #[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<String>, | ||||
|     #[serde(rename(serialize = "@bus"))] | ||||
|     pub r#bus: String, | ||||
|     #[serde( | ||||
|         default, | ||||
|         skip_serializing_if = "Option::is_none", | ||||
|         rename(serialize = "@target") | ||||
|     )] | ||||
|     pub r#target: Option<String>, | ||||
|     #[serde( | ||||
|         default, | ||||
|         skip_serializing_if = "Option::is_none", | ||||
|         rename(serialize = "@unit") | ||||
|     )] | ||||
|     pub r#unit: Option<String>, | ||||
| } | ||||
|  | ||||
| /// 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<DomainCPUTopology>, | ||||
| } | ||||
|  | ||||
| /// 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<XMLUuid>, | ||||
|     pub genid: Option<uuid::Uuid>, | ||||
|     pub title: Option<String>, | ||||
|     pub description: Option<String>, | ||||
|     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<String> { | ||||
|         // 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("<disk").unwrap(); | ||||
|             devices_xml.push(disk_xml[start_offset..].to_string()); | ||||
|         } | ||||
|         for network in self.devices.net_interfaces { | ||||
|             let network_xml = serde_xml_rs::to_string(&network)?; | ||||
|             let start_offset = network_xml.find("<interface").unwrap(); | ||||
|             devices_xml.push(network_xml[start_offset..].to_string()); | ||||
|         } | ||||
|         for input in self.devices.inputs { | ||||
|             let input_xml = serde_xml_rs::to_string(&input)?; | ||||
|             let start_offset = input_xml.find("<input").unwrap(); | ||||
|             devices_xml.push(input_xml[start_offset..].to_string()); | ||||
|         } | ||||
|  | ||||
|         self.devices.disks = vec![]; | ||||
|         self.devices.net_interfaces = vec![]; | ||||
|         self.devices.inputs = vec![]; | ||||
|  | ||||
|         let mut xml = serde_xml_rs::to_string(&self)?; | ||||
|         let disks_xml = devices_xml.join("\n"); | ||||
|         xml = xml.replacen("<devices>", &format!("<devices>{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<NetworkDHCPXML>, | ||||
| } | ||||
|  | ||||
| impl NetworkIPXML { | ||||
|     pub fn into_xml(mut self) -> anyhow::Result<String> { | ||||
|         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("<host").unwrap(); | ||||
|                 hosts_xml.push(host_xml[start_offset..].to_string()); | ||||
|             } | ||||
|  | ||||
|             dhcp.hosts = vec![]; | ||||
|         } | ||||
|  | ||||
|         let mut res = serde_xml_rs::to_string(&self)?; | ||||
|         let hosts_xml = hosts_xml.join("\n"); | ||||
|         res = res.replace("</dhcp>", &format!("{hosts_xml}</dhcp>")); | ||||
|         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<NetworkDHCPHostXML>, | ||||
| } | ||||
|  | ||||
| #[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<XMLUuid>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub title: Option<String>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub description: Option<String>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub forward: Option<NetworkForwardXML>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub bridge: Option<NetworkBridgeXML>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub dns: Option<NetworkDNSXML>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub domain: Option<NetworkDomainXML>, | ||||
|     #[serde(default, rename = "ip")] | ||||
|     pub ips: Vec<NetworkIPXML>, | ||||
| } | ||||
|  | ||||
| impl NetworkXML { | ||||
|     pub fn into_xml(mut self) -> anyhow::Result<String> { | ||||
|         // 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("<ip").unwrap(); | ||||
|             ips_xml.push(ip_xml[start_offset..].to_string()); | ||||
|         } | ||||
|         self.ips = vec![]; | ||||
|  | ||||
|         let mut network_xml = serde_xml_rs::to_string(&self)?; | ||||
|         log::trace!("Serialize network XML start: {network_xml}"); | ||||
|  | ||||
|         let ips_xml = ips_xml.join("\n"); | ||||
|         network_xml = network_xml.replacen("</network>", &format!("{ips_xml}</network>"), 1); | ||||
|         Ok(network_xml) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										346
									
								
								virtweb_backend/src/libvirt_lib_structures/domain.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										346
									
								
								virtweb_backend/src/libvirt_lib_structures/domain.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,346 @@ | ||||
| use crate::libvirt_lib_structures::XMLUuid; | ||||
|  | ||||
| /// OS information | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "os")] | ||||
| pub struct OSXML { | ||||
|     #[serde(rename = "@firmware", default)] | ||||
|     pub firmware: String, | ||||
|     pub r#type: OSTypeXML, | ||||
|     pub loader: Option<OSLoaderXML>, | ||||
| } | ||||
|  | ||||
| /// OS Type information | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "os")] | ||||
| pub struct OSTypeXML { | ||||
|     #[serde(rename = "@arch")] | ||||
|     pub arch: String, | ||||
|     #[serde(rename = "@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 = "@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 = "@address")] | ||||
|     pub address: String, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "source")] | ||||
| pub struct NetIntSourceXML { | ||||
|     #[serde(rename = "@network")] | ||||
|     pub network: String, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "model")] | ||||
| pub struct NetIntModelXML { | ||||
|     #[serde(rename = "@type")] | ||||
|     pub r#type: String, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "filterref")] | ||||
| pub struct NetIntFilterParameterXML { | ||||
|     #[serde(rename = "@name")] | ||||
|     pub name: String, | ||||
|     #[serde(rename = "@value")] | ||||
|     pub value: String, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "filterref")] | ||||
| pub struct NetIntfilterRefXML { | ||||
|     #[serde(rename = "@filter")] | ||||
|     pub filter: String, | ||||
|     #[serde(rename = "parameter", default)] | ||||
|     pub parameters: Vec<NetIntFilterParameterXML>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "interface")] | ||||
| pub struct DomainNetInterfaceXML { | ||||
|     #[serde(rename = "@type")] | ||||
|     pub r#type: String, | ||||
|     pub mac: NetMacAddress, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub source: Option<NetIntSourceXML>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub model: Option<NetIntModelXML>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub filterref: Option<NetIntfilterRefXML>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "input")] | ||||
| pub struct DomainInputXML { | ||||
|     #[serde(rename = "@type")] | ||||
|     pub r#type: String, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "backend")] | ||||
| pub struct TPMBackendXML { | ||||
|     #[serde(rename = "@type")] | ||||
|     pub r#type: String, | ||||
|  | ||||
|     #[serde(rename = "@version")] | ||||
|     pub r#version: String, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "tpm")] | ||||
| pub struct TPMDeviceXML { | ||||
|     #[serde(rename = "@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<GraphicsXML>, | ||||
|  | ||||
|     /// Graphics (used for VNC) | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub video: Option<VideoXML>, | ||||
|  | ||||
|     /// Disks (used for storage) | ||||
|     #[serde(default, rename = "disk", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub disks: Vec<DiskXML>, | ||||
|  | ||||
|     /// Networks cards | ||||
|     #[serde(default, rename = "interface", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub net_interfaces: Vec<DomainNetInterfaceXML>, | ||||
|  | ||||
|     /// Input devices | ||||
|     #[serde(default, rename = "input", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub inputs: Vec<DomainInputXML>, | ||||
|  | ||||
|     /// TPM device | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub tpm: Option<TPMDeviceXML>, | ||||
| } | ||||
|  | ||||
| /// Graphics information | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "graphics")] | ||||
| pub struct GraphicsXML { | ||||
|     #[serde(rename = "@type")] | ||||
|     pub r#type: String, | ||||
|     #[serde(rename = "@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 = "@type")] | ||||
|     pub r#type: String, | ||||
| } | ||||
|  | ||||
| /// Disk information | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "disk")] | ||||
| pub struct DiskXML { | ||||
|     #[serde(rename = "@type")] | ||||
|     pub r#type: String, | ||||
|     #[serde(rename = "@device")] | ||||
|     pub r#device: String, | ||||
|  | ||||
|     pub driver: DiskDriverXML, | ||||
|     pub source: DiskSourceXML, | ||||
|     pub target: DiskTargetXML, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub readonly: Option<DiskReadOnlyXML>, | ||||
|     pub boot: DiskBootXML, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub address: Option<DiskAddressXML>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "driver")] | ||||
| pub struct DiskDriverXML { | ||||
|     #[serde(rename = "@name")] | ||||
|     pub name: String, | ||||
|     #[serde(rename = "@type")] | ||||
|     pub r#type: String, | ||||
|     #[serde(default, rename = "@cache")] | ||||
|     pub r#cache: String, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "source")] | ||||
| pub struct DiskSourceXML { | ||||
|     #[serde(rename = "@file")] | ||||
|     pub file: String, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "target")] | ||||
| pub struct DiskTargetXML { | ||||
|     #[serde(rename = "@dev")] | ||||
|     pub dev: String, | ||||
|     #[serde(rename = "@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 = "@order")] | ||||
|     pub order: String, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "address")] | ||||
| pub struct DiskAddressXML { | ||||
|     #[serde(rename = "@type")] | ||||
|     pub r#type: String, | ||||
|     #[serde( | ||||
|         default, | ||||
|         skip_serializing_if = "Option::is_none", | ||||
|         rename = "@controller" | ||||
|     )] | ||||
|     pub r#controller: Option<String>, | ||||
|     #[serde(rename = "@bus")] | ||||
|     pub r#bus: String, | ||||
|     #[serde(default, skip_serializing_if = "Option::is_none", rename = "@target")] | ||||
|     pub r#target: Option<String>, | ||||
|     #[serde(default, skip_serializing_if = "Option::is_none", rename = "@unit")] | ||||
|     pub r#unit: Option<String>, | ||||
| } | ||||
|  | ||||
| /// Domain RAM information | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "memory")] | ||||
| pub struct DomainMemoryXML { | ||||
|     #[serde(rename = "@unit")] | ||||
|     pub unit: String, | ||||
|  | ||||
|     #[serde(rename = "$value")] | ||||
|     pub memory: usize, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "topology")] | ||||
| pub struct DomainCPUTopology { | ||||
|     #[serde(rename = "@sockets")] | ||||
|     pub sockets: usize, | ||||
|     #[serde(rename = "@cores")] | ||||
|     pub cores: usize, | ||||
|     #[serde(rename = "@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 = "@mode")] | ||||
|     pub mode: String, | ||||
|     pub topology: Option<DomainCPUTopology>, | ||||
| } | ||||
|  | ||||
| /// Domain information, see https://libvirt.org/formatdomain.html | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "domain")] | ||||
| pub struct DomainXML { | ||||
|     /// Domain type (kvm) | ||||
|     #[serde(rename = "@type")] | ||||
|     pub r#type: String, | ||||
|  | ||||
|     pub name: String, | ||||
|     pub uuid: Option<XMLUuid>, | ||||
|     pub genid: Option<uuid::Uuid>, | ||||
|     pub title: Option<String>, | ||||
|     pub description: Option<String>, | ||||
|     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 { | ||||
|     /// Decode Domain structure from XML definition | ||||
|     pub fn parse_xml(xml: &str) -> anyhow::Result<Self> { | ||||
|         Ok(quick_xml::de::from_str(xml)?) | ||||
|     } | ||||
|  | ||||
|     /// Turn this domain into its XML definition | ||||
|     pub fn as_xml(&self) -> anyhow::Result<String> { | ||||
|         Ok(quick_xml::se::to_string(self)?) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Domain state | ||||
| #[derive(serde::Serialize, Debug, Copy, Clone)] | ||||
| pub enum DomainState { | ||||
|     NoState, | ||||
|     Running, | ||||
|     Blocked, | ||||
|     Paused, | ||||
|     Shutdown, | ||||
|     Shutoff, | ||||
|     Crashed, | ||||
|     PowerManagementSuspended, | ||||
|     Other, | ||||
| } | ||||
							
								
								
									
										24
									
								
								virtweb_backend/src/libvirt_lib_structures/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								virtweb_backend/src/libvirt_lib_structures/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -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<Self> { | ||||
|         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; | ||||
							
								
								
									
										131
									
								
								virtweb_backend/src/libvirt_lib_structures/network.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								virtweb_backend/src/libvirt_lib_structures/network.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,131 @@ | ||||
| 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 = "@mode")] | ||||
|     pub mode: String, | ||||
|     #[serde(default, rename = "@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 = "@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 = "@addr")] | ||||
|     pub addr: Ipv4Addr, | ||||
| } | ||||
|  | ||||
| /// Network DNS information | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "domain")] | ||||
| pub struct NetworkDomainXML { | ||||
|     #[serde(rename = "@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 = "@family")] | ||||
|     pub family: String, | ||||
|     #[serde(rename = "@address")] | ||||
|     pub address: IpAddr, | ||||
|     /// Network Prefix | ||||
|     #[serde(rename = "@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 = "@netmask", default = "invalid_ip", skip_serializing)] | ||||
|     pub netmask: IpAddr, | ||||
|     pub dhcp: Option<NetworkDHCPXML>, | ||||
| } | ||||
|  | ||||
| #[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<NetworkDHCPHostXML>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "host")] | ||||
| pub struct NetworkDHCPHostXML { | ||||
|     #[serde(rename = "@mac", default, skip_serializing_if = "Option::is_none")] | ||||
|     pub mac: Option<String>, | ||||
|     #[serde(rename = "@name")] | ||||
|     pub name: String, | ||||
|     #[serde(rename = "@ip")] | ||||
|     pub ip: IpAddr, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "dhcp")] | ||||
| pub struct NetworkDHCPRangeXML { | ||||
|     #[serde(rename = "@start")] | ||||
|     pub start: IpAddr, | ||||
|     #[serde(rename = "@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<XMLUuid>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub title: Option<String>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub description: Option<String>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub forward: Option<NetworkForwardXML>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub bridge: Option<NetworkBridgeXML>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub dns: Option<NetworkDNSXML>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub domain: Option<NetworkDomainXML>, | ||||
|     #[serde(default, rename = "ip")] | ||||
|     pub ips: Vec<NetworkIPXML>, | ||||
| } | ||||
|  | ||||
| impl NetworkXML { | ||||
|     pub fn parse_xml(xml: &str) -> anyhow::Result<Self> { | ||||
|         Ok(quick_xml::de::from_str(xml)?) | ||||
|     } | ||||
|  | ||||
|     pub fn as_xml(&self) -> anyhow::Result<String> { | ||||
|         Ok(quick_xml::se::to_string(self)?) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										211
									
								
								virtweb_backend/src/libvirt_lib_structures/nwfilter.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										211
									
								
								virtweb_backend/src/libvirt_lib_structures/nwfilter.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,211 @@ | ||||
| use crate::libvirt_lib_structures::XMLUuid; | ||||
| use std::fmt::Display; | ||||
| use std::net::{Ipv4Addr, Ipv6Addr}; | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "filterref")] | ||||
| pub struct NetworkFilterRefXML { | ||||
|     #[serde(rename = "@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 = "@srcmacaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcmacaddr: Option<String>, | ||||
|     #[serde(rename = "@scmacmask", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcmacmask: Option<String>, | ||||
|     #[serde(rename = "@dstmacaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstmacaddr: Option<String>, | ||||
|     #[serde(rename = "@dstmacmask", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstmacmask: Option<String>, | ||||
|     #[serde(rename = "@comment", skip_serializing_if = "Option::is_none")] | ||||
|     pub comment: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "arp")] | ||||
| pub struct NetworkFilterRuleProtocolArpXML { | ||||
|     #[serde(rename = "@srcmacaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcmacaddr: Option<String>, | ||||
|     #[serde(rename = "@srcmacmask", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcmacmask: Option<String>, | ||||
|     #[serde(rename = "@dstmacaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstmacaddr: Option<String>, | ||||
|     #[serde(rename = "@dstmacmask", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstmacmask: Option<String>, | ||||
|     #[serde(rename = "@arpsrcipaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub arpsrcipaddr: Option<String>, | ||||
|     #[serde(rename = "@arpsrcipmask", skip_serializing_if = "Option::is_none")] | ||||
|     pub arpsrcipmask: Option<u8>, | ||||
|     #[serde(rename = "@arpdstipaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub arpdstipaddr: Option<String>, | ||||
|     #[serde(rename = "@arpdstipmask", skip_serializing_if = "Option::is_none")] | ||||
|     pub arpdstipmask: Option<u8>, | ||||
|  | ||||
|     #[serde(rename = "@comment", skip_serializing_if = "Option::is_none")] | ||||
|     pub comment: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "ipvx")] | ||||
| pub struct NetworkFilterRuleProtocolIpvx { | ||||
|     #[serde(rename = "@srcmacaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcmacaddr: Option<String>, | ||||
|     #[serde(rename = "@srcmacmask", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcmacmask: Option<String>, | ||||
|     #[serde(rename = "@dstmacaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstmacaddr: Option<String>, | ||||
|     #[serde(rename = "@dstmacmask", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstmacmask: Option<String>, | ||||
|     #[serde(rename = "@srcipaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcipaddr: Option<String>, | ||||
|     #[serde(rename = "@srcipmask", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcipmask: Option<u8>, | ||||
|     #[serde(rename = "@dstipaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstipaddr: Option<String>, | ||||
|     #[serde(rename = "@dstipmask", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstipmask: Option<u8>, | ||||
|  | ||||
|     #[serde(rename = "@comment", skip_serializing_if = "Option::is_none")] | ||||
|     pub comment: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "layer4")] | ||||
| pub struct NetworkFilterRuleProtocolLayer4<IPv> { | ||||
|     #[serde(rename = "@srcmacaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcmacaddr: Option<String>, | ||||
|     #[serde(rename = "@srcipaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcipaddr: Option<IPv>, | ||||
|     #[serde(rename = "@srcipmask", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcipmask: Option<u8>, | ||||
|     #[serde(rename = "@dstipaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstipaddr: Option<IPv>, | ||||
|     #[serde(rename = "@dstipmask", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstipmask: Option<u8>, | ||||
|     /// Start of range of source IP address | ||||
|     #[serde(rename = "@srcipfrom", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcipfrom: Option<IPv>, | ||||
|     /// End of range of source IP address | ||||
|     #[serde(rename = "@srcipto", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcipto: Option<IPv>, | ||||
|     /// Start of range of destination IP address | ||||
|     #[serde(rename = "@dstipfrom", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstipfrom: Option<IPv>, | ||||
|     /// End of range of destination IP address | ||||
|     #[serde(rename = "@dstipto", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstipto: Option<IPv>, | ||||
|     #[serde(rename = "@srcportstart", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcportstart: Option<u16>, | ||||
|     #[serde(rename = "@srcportend", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcportend: Option<u16>, | ||||
|     #[serde(rename = "@dstportstart", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstportstart: Option<u16>, | ||||
|     #[serde(rename = "@dstportend", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstportend: Option<u16>, | ||||
|     #[serde(rename = "@state", skip_serializing_if = "Option::is_none")] | ||||
|     pub state: Option<String>, | ||||
|  | ||||
|     #[serde(rename = "@comment", skip_serializing_if = "Option::is_none")] | ||||
|     pub comment: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Default)] | ||||
| #[serde(rename = "rule")] | ||||
| pub struct NetworkFilterRuleXML { | ||||
|     #[serde(rename = "@action")] | ||||
|     pub action: String, | ||||
|     #[serde(rename = "@direction")] | ||||
|     pub direction: String, | ||||
|     #[serde(rename = "@priority")] | ||||
|     pub priority: Option<i32>, | ||||
|  | ||||
|     /// Match all protocols | ||||
|     #[serde(default, rename = "all", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub all_selectors: Vec<NetworkFilterRuleProtocolAll>, | ||||
|  | ||||
|     /// Match mac protocol | ||||
|     #[serde(default, rename = "mac", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub mac_selectors: Vec<NetworkFilterRuleProtocolMac>, | ||||
|  | ||||
|     /// Match arp protocol | ||||
|     #[serde(default, rename = "arp", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub arp_selectors: Vec<NetworkFilterRuleProtocolArpXML>, | ||||
|  | ||||
|     /// Match rarp protocol | ||||
|     #[serde(default, rename = "rarp", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub rarp_selectors: Vec<NetworkFilterRuleProtocolArpXML>, | ||||
|  | ||||
|     /// Match IPv4 protocol | ||||
|     #[serde(default, rename = "ip", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub ipv4_selectors: Vec<NetworkFilterRuleProtocolIpvx>, | ||||
|  | ||||
|     /// Match IPv6 protocol | ||||
|     #[serde(default, rename = "ipv6", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub ipv6_selectors: Vec<NetworkFilterRuleProtocolIpvx>, | ||||
|  | ||||
|     /// Match TCP protocol | ||||
|     #[serde(default, rename = "tcp", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub tcp_selectors: Vec<NetworkFilterRuleProtocolLayer4<Ipv4Addr>>, | ||||
|  | ||||
|     /// Match UDP protocol | ||||
|     #[serde(default, rename = "udp", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub udp_selectors: Vec<NetworkFilterRuleProtocolLayer4<Ipv4Addr>>, | ||||
|  | ||||
|     /// Match SCTP protocol | ||||
|     #[serde(default, rename = "sctp", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub sctp_selectors: Vec<NetworkFilterRuleProtocolLayer4<Ipv4Addr>>, | ||||
|  | ||||
|     /// Match ICMP protocol | ||||
|     #[serde(default, rename = "icmp", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub icmp_selectors: Vec<NetworkFilterRuleProtocolLayer4<Ipv4Addr>>, | ||||
|  | ||||
|     /// Match TCP IPv6 protocol | ||||
|     #[serde(default, rename = "tcp-ipv6", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub tcp_ipv6_selectors: Vec<NetworkFilterRuleProtocolLayer4<Ipv6Addr>>, | ||||
|  | ||||
|     /// Match UDP IPv6 protocol | ||||
|     #[serde(default, rename = "udp-ipv6", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub udp_ipv6_selectors: Vec<NetworkFilterRuleProtocolLayer4<Ipv6Addr>>, | ||||
|  | ||||
|     /// Match SCTP IPv6 protocol | ||||
|     #[serde(default, rename = "sctp-ipv6", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub sctp_ipv6_selectors: Vec<NetworkFilterRuleProtocolLayer4<Ipv6Addr>>, | ||||
|  | ||||
|     /// Match ICMP IPv6 protocol | ||||
|     #[serde(default, rename = "icmpv6", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub imcp_ipv6_selectors: Vec<NetworkFilterRuleProtocolLayer4<Ipv6Addr>>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "filter")] | ||||
| pub struct NetworkFilterXML { | ||||
|     #[serde(rename = "@name")] | ||||
|     pub name: String, | ||||
|     #[serde(rename = "@chain", skip_serializing_if = "Option::is_none", default)] | ||||
|     pub chain: Option<String>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none", rename = "@priority", default)] | ||||
|     pub priority: Option<i32>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub uuid: Option<XMLUuid>, | ||||
|     #[serde(default, rename = "filterref")] | ||||
|     pub filterrefs: Vec<NetworkFilterRefXML>, | ||||
|     #[serde(default, rename = "rule", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub rules: Vec<NetworkFilterRuleXML>, | ||||
| } | ||||
|  | ||||
| impl NetworkFilterXML { | ||||
|     pub fn parse_xml<D: Display>(xml: D) -> anyhow::Result<Self> { | ||||
|         Ok(quick_xml::de::from_str(&xml.to_string())?) | ||||
|     } | ||||
|  | ||||
|     pub fn into_xml(self) -> anyhow::Result<String> { | ||||
|         Ok(quick_xml::se::to_string(&self)?) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										23
									
								
								virtweb_backend/src/libvirt_rest_structures/hypervisor.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								virtweb_backend/src/libvirt_rest_structures/hypervisor.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -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, | ||||
| } | ||||
							
								
								
									
										16
									
								
								virtweb_backend/src/libvirt_rest_structures/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								virtweb_backend/src/libvirt_rest_structures/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| 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), | ||||
|     #[error("NetworkFilterExtractionError: {0}")] | ||||
|     NetworkFilterExtraction(String), | ||||
| } | ||||
							
								
								
									
										258
									
								
								virtweb_backend/src/libvirt_rest_structures/net.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										258
									
								
								virtweb_backend/src/libvirt_rest_structures/net.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -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<DHCPv4HostReservation>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] | ||||
| pub struct IPV4Config { | ||||
|     bridge_address: Ipv4Addr, | ||||
|     prefix: u32, | ||||
|     dhcp: Option<IPv4DHCPConfig>, | ||||
| } | ||||
|  | ||||
| #[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<DHCPv6HostReservation>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] | ||||
| pub struct IPV6Config { | ||||
|     bridge_address: Ipv6Addr, | ||||
|     prefix: u32, | ||||
|     dhcp: Option<IPv6DHCPConfig>, | ||||
| } | ||||
|  | ||||
| /// Network configuration | ||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] | ||||
| pub struct NetworkInfo { | ||||
|     pub name: String, | ||||
|     pub uuid: Option<XMLUuid>, | ||||
|     title: Option<String>, | ||||
|     description: Option<String>, | ||||
|     forward_mode: NetworkForwardMode, | ||||
|     device: Option<String>, | ||||
|     bridge_name: Option<String>, | ||||
|     dns_server: Option<Ipv4Addr>, | ||||
|     domain: Option<String>, | ||||
|     ip_v4: Option<IPV4Config>, | ||||
|     ip_v6: Option<IPV6Config>, | ||||
| } | ||||
|  | ||||
| impl NetworkInfo { | ||||
|     pub fn as_virt_network(&self) -> anyhow::Result<NetworkXML> { | ||||
|         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: Some(c.mac.to_string()), | ||||
|                             name: c.name.to_string(), | ||||
|                             ip: c.ip.into(), | ||||
|                         }) | ||||
|                         .collect::<Vec<_>>(), | ||||
|                 }), | ||||
|             }) | ||||
|         } | ||||
|  | ||||
|         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: None, | ||||
|                             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<Self> { | ||||
|         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.clone().unwrap_or_default(), | ||||
|                                 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(), | ||||
|                     }), | ||||
|                 }), | ||||
|         }) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										847
									
								
								virtweb_backend/src/libvirt_rest_structures/nw_filter.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										847
									
								
								virtweb_backend/src/libvirt_rest_structures/nw_filter.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,847 @@ | ||||
| use crate::libvirt_lib_structures::nwfilter::{ | ||||
|     NetworkFilterRefXML, NetworkFilterRuleProtocolAll, NetworkFilterRuleProtocolArpXML, | ||||
|     NetworkFilterRuleProtocolIpvx, NetworkFilterRuleProtocolLayer4, NetworkFilterRuleProtocolMac, | ||||
|     NetworkFilterRuleXML, NetworkFilterXML, | ||||
| }; | ||||
| use crate::libvirt_lib_structures::XMLUuid; | ||||
| use crate::libvirt_rest_structures::LibVirtStructError; | ||||
| use crate::libvirt_rest_structures::LibVirtStructError::{ | ||||
|     NetworkFilterExtraction, StructureExtraction, | ||||
| }; | ||||
| use crate::utils::net_utils; | ||||
| use lazy_regex::regex; | ||||
| use std::net::{Ipv4Addr, Ipv6Addr}; | ||||
|  | ||||
| pub fn is_var_def(var: &str) -> bool { | ||||
|     lazy_regex::regex!("^\\$[a-zA-Z0-9_]+$").is_match(var) | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| pub struct NetworkFilterName(pub String); | ||||
|  | ||||
| impl NetworkFilterName { | ||||
|     pub fn is_valid(&self) -> bool { | ||||
|         regex!("^[a-zA-Z0-9-_]+$").is_match(&self.0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| pub struct NetworkFilterMacAddressOrVar(pub String); | ||||
|  | ||||
| impl NetworkFilterMacAddressOrVar { | ||||
|     pub fn is_valid(&self) -> bool { | ||||
|         is_var_def(&self.0) || net_utils::is_mac_address_valid(&self.0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<&String> for NetworkFilterMacAddressOrVar { | ||||
|     fn from(value: &String) -> Self { | ||||
|         Self(value.to_string()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn extract_mac_address_or_var( | ||||
|     n: &Option<NetworkFilterMacAddressOrVar>, | ||||
| ) -> anyhow::Result<Option<String>> { | ||||
|     if let Some(mac) = n { | ||||
|         if !mac.is_valid() { | ||||
|             return Err(NetworkFilterExtraction(format!( | ||||
|                 "Invalid mac address or variable! {}", | ||||
|                 mac.0 | ||||
|             )) | ||||
|             .into()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(n.as_ref().map(|n| n.0.to_string())) | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| pub struct NetworkFilterIPOrVar<const V: usize>(pub String); | ||||
| pub type NetworkFilterIPv4OrVar = NetworkFilterIPOrVar<4>; | ||||
|  | ||||
| impl<const V: usize> NetworkFilterIPOrVar<V> { | ||||
|     pub fn is_valid(&self) -> bool { | ||||
|         is_var_def(&self.0) | ||||
|             || match V { | ||||
|                 4 => net_utils::is_ipv4_address_valid(&self.0), | ||||
|                 6 => net_utils::is_ipv6_address_valid(&self.0), | ||||
|                 _ => panic!("Unsupported IP version!"), | ||||
|             } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl<const V: usize> From<&String> for NetworkFilterIPOrVar<V> { | ||||
|     fn from(value: &String) -> Self { | ||||
|         if V != 4 && V != 6 { | ||||
|             panic!("Unsupported IP version!"); | ||||
|         } | ||||
|         Self(value.to_string()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn extract_ip_or_var<const V: usize>( | ||||
|     n: &Option<NetworkFilterIPOrVar<V>>, | ||||
| ) -> anyhow::Result<Option<String>> { | ||||
|     if let Some(ip) = n { | ||||
|         if !ip.is_valid() { | ||||
|             return Err(NetworkFilterExtraction(format!( | ||||
|                 "Invalid IPv{V} address or variable! {}", | ||||
|                 ip.0 | ||||
|             )) | ||||
|             .into()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(n.as_ref().map(|n| n.0.to_string())) | ||||
| } | ||||
|  | ||||
| fn extract_ip_mask<const V: usize>(n: Option<u8>) -> anyhow::Result<Option<u8>> { | ||||
|     if let Some(mask) = n { | ||||
|         if !net_utils::is_mask_valid(V, mask) { | ||||
|             return Err(NetworkFilterExtraction(format!("Invalid IPv{V} mask! {mask}")).into()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(n) | ||||
| } | ||||
|  | ||||
| fn extract_nw_filter_comment(n: &Option<String>) -> anyhow::Result<Option<String>> { | ||||
|     if let Some(comment) = n { | ||||
|         if comment.len() > 256 || comment.contains('\"') || comment.contains('\n') { | ||||
|             return Err(NetworkFilterExtraction(format!("Invalid comment! {}", comment)).into()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(n.clone()) | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Copy, Clone)] | ||||
| #[serde(rename_all = "lowercase")] | ||||
| pub enum NetworkFilterChainProtocol { | ||||
|     Root, | ||||
|     Mac, | ||||
|     STP, | ||||
|     VLAN, | ||||
|     ARP, | ||||
|     RARP, | ||||
|     IPv4, | ||||
|     IPv6, | ||||
| } | ||||
|  | ||||
| impl NetworkFilterChainProtocol { | ||||
|     pub fn from_xml(xml: &str) -> anyhow::Result<Self> { | ||||
|         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<String>, | ||||
| } | ||||
|  | ||||
| impl NetworkFilterChain { | ||||
|     pub fn from_xml(xml: &str) -> anyhow::Result<Self> { | ||||
|         Ok(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()), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Copy, Clone)] | ||||
| #[serde(rename_all = "lowercase")] | ||||
| 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, | ||||
| } | ||||
|  | ||||
| impl NetworkFilterAction { | ||||
|     pub fn from_xml(xml: &str) -> anyhow::Result<Self> { | ||||
|         Ok(match xml { | ||||
|             "drop" => Self::Drop, | ||||
|             "reject" => Self::Reject, | ||||
|             "accept" => Self::Accept, | ||||
|             "return" => Self::Return, | ||||
|             "continue" => Self::Continue, | ||||
|             s => { | ||||
|                 return Err(LibVirtStructError::ParseFilteringChain(format!( | ||||
|                     "Unkown filter action {s}!" | ||||
|                 )) | ||||
|                 .into()); | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     pub fn to_xml(&self) -> String { | ||||
|         match self { | ||||
|             Self::Drop => "drop", | ||||
|             Self::Reject => "reject", | ||||
|             Self::Accept => "accept", | ||||
|             Self::Return => "return", | ||||
|             Self::Continue => "continue", | ||||
|         } | ||||
|         .to_string() | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| #[serde(rename_all = "lowercase")] | ||||
| pub enum NetworkFilterDirection { | ||||
|     In, | ||||
|     Out, | ||||
|     InOut, | ||||
| } | ||||
|  | ||||
| impl NetworkFilterDirection { | ||||
|     pub fn from_xml(xml: &str) -> anyhow::Result<Self> { | ||||
|         Ok(match xml { | ||||
|             "in" => Self::In, | ||||
|             "out" => Self::Out, | ||||
|             "inout" => Self::InOut, | ||||
|             s => { | ||||
|                 return Err(LibVirtStructError::ParseFilteringChain(format!( | ||||
|                     "Unkown filter direction {s}!" | ||||
|                 )) | ||||
|                 .into()); | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     pub fn to_xml(&self) -> String { | ||||
|         match self { | ||||
|             NetworkFilterDirection::In => "in", | ||||
|             NetworkFilterDirection::Out => "out", | ||||
|             NetworkFilterDirection::InOut => "inout", | ||||
|         } | ||||
|         .to_string() | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Copy, Clone)] | ||||
| pub enum Layer4State { | ||||
|     NEW, | ||||
|     ESTABLISHED, | ||||
|     RELATED, | ||||
|     INVALID, | ||||
|     NONE, | ||||
| } | ||||
|  | ||||
| impl Layer4State { | ||||
|     pub fn from_xml(xml: &str) -> anyhow::Result<Self> { | ||||
|         Ok(match xml { | ||||
|             "NEW" => Self::NEW, | ||||
|             "ESTABLISHED" => Self::ESTABLISHED, | ||||
|             "RELATED" => Self::RELATED, | ||||
|             "INVALID" => Self::INVALID, | ||||
|             "NONE" => Self::NONE, | ||||
|             s => { | ||||
|                 return Err(LibVirtStructError::ParseFilteringChain(format!( | ||||
|                     "Unkown layer4 state '{s}'!" | ||||
|                 )) | ||||
|                 .into()); | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     pub fn to_xml(&self) -> String { | ||||
|         match self { | ||||
|             Self::NEW => "NEW", | ||||
|             Self::ESTABLISHED => "ESTABLISHED", | ||||
|             Self::RELATED => "RELATED", | ||||
|             Self::INVALID => "INVALID", | ||||
|             Self::NONE => "NONE", | ||||
|         } | ||||
|         .to_string() | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| pub struct NetworkSelectorMac { | ||||
|     src_mac_addr: Option<NetworkFilterMacAddressOrVar>, | ||||
|     src_mac_mask: Option<NetworkFilterMacAddressOrVar>, | ||||
|     dst_mac_addr: Option<NetworkFilterMacAddressOrVar>, | ||||
|     dst_mac_mask: Option<NetworkFilterMacAddressOrVar>, | ||||
|     comment: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| pub struct NetworkSelectorARP { | ||||
|     srcmacaddr: Option<NetworkFilterMacAddressOrVar>, | ||||
|     srcmacmask: Option<NetworkFilterMacAddressOrVar>, | ||||
|     dstmacaddr: Option<NetworkFilterMacAddressOrVar>, | ||||
|     dstmacmask: Option<NetworkFilterMacAddressOrVar>, | ||||
|     arpsrcipaddr: Option<NetworkFilterIPv4OrVar>, | ||||
|     arpsrcipmask: Option<u8>, | ||||
|     arpdstipaddr: Option<NetworkFilterIPv4OrVar>, | ||||
|     arpdstipmask: Option<u8>, | ||||
|     comment: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| pub struct NetworkFilterSelectorIP<const V: usize> { | ||||
|     srcmacaddr: Option<NetworkFilterMacAddressOrVar>, | ||||
|     srcmacmask: Option<NetworkFilterMacAddressOrVar>, | ||||
|     dstmacaddr: Option<NetworkFilterMacAddressOrVar>, | ||||
|     dstmacmask: Option<NetworkFilterMacAddressOrVar>, | ||||
|     srcipaddr: Option<NetworkFilterIPOrVar<V>>, | ||||
|     srcipmask: Option<u8>, | ||||
|     dstipaddr: Option<NetworkFilterIPOrVar<V>>, | ||||
|     dstipmask: Option<u8>, | ||||
|     comment: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| pub struct NetworkFilterSelectorLayer4<IPv> { | ||||
|     srcmacaddr: Option<NetworkFilterMacAddressOrVar>, | ||||
|     srcipaddr: Option<IPv>, | ||||
|     srcipmask: Option<u8>, | ||||
|     dstipaddr: Option<IPv>, | ||||
|     dstipmask: Option<u8>, | ||||
|     /// Start of range of source IP address | ||||
|     srcipfrom: Option<IPv>, | ||||
|     /// End of range of source IP address | ||||
|     srcipto: Option<IPv>, | ||||
|     /// Start of range of destination IP address | ||||
|     dstipfrom: Option<IPv>, | ||||
|     /// End of range of destination IP address | ||||
|     dstipto: Option<IPv>, | ||||
|     srcportstart: Option<u16>, | ||||
|     srcportend: Option<u16>, | ||||
|     dstportstart: Option<u16>, | ||||
|     dstportend: Option<u16>, | ||||
|     state: Option<Layer4State>, | ||||
|     comment: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| #[serde(tag = "type", rename_all = "lowercase")] | ||||
| pub enum NetworkFilterSelector { | ||||
|     All, | ||||
|     Mac(NetworkSelectorMac), | ||||
|     Arp(NetworkSelectorARP), | ||||
|     Rarp(NetworkSelectorARP), | ||||
|     IPv4(NetworkFilterSelectorIP<4>), | ||||
|     IPv6(NetworkFilterSelectorIP<6>), | ||||
|     TCP(NetworkFilterSelectorLayer4<Ipv4Addr>), | ||||
|     UDP(NetworkFilterSelectorLayer4<Ipv4Addr>), | ||||
|     SCTP(NetworkFilterSelectorLayer4<Ipv4Addr>), | ||||
|     ICMP(NetworkFilterSelectorLayer4<Ipv4Addr>), | ||||
|     TCPipv6(NetworkFilterSelectorLayer4<Ipv6Addr>), | ||||
|     UDPipv6(NetworkFilterSelectorLayer4<Ipv6Addr>), | ||||
|     SCTPipv6(NetworkFilterSelectorLayer4<Ipv6Addr>), | ||||
|     ICMPipv6(NetworkFilterSelectorLayer4<Ipv6Addr>), | ||||
| } | ||||
|  | ||||
| #[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<i32>, | ||||
|     selectors: Vec<NetworkFilterSelector>, | ||||
| } | ||||
|  | ||||
| /// Network filter definition | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| pub struct NetworkFilter { | ||||
|     pub name: NetworkFilterName, | ||||
|     chain: Option<NetworkFilterChain>, | ||||
|     priority: Option<i32>, | ||||
|     pub uuid: Option<XMLUuid>, | ||||
|     /// Referenced filters rules | ||||
|     join_filters: Vec<NetworkFilterName>, | ||||
|     rules: Vec<NetworkFilterRule>, | ||||
| } | ||||
|  | ||||
| impl NetworkFilter { | ||||
|     fn lib2rest_process_all_rule(_n: &NetworkFilterRuleProtocolAll) -> NetworkFilterSelector { | ||||
|         NetworkFilterSelector::All | ||||
|     } | ||||
|  | ||||
|     fn lib2rest_process_mac_rule(n: &NetworkFilterRuleProtocolMac) -> NetworkFilterSelector { | ||||
|         NetworkFilterSelector::Mac(NetworkSelectorMac { | ||||
|             src_mac_addr: n.srcmacaddr.as_ref().map(|v| v.into()), | ||||
|             src_mac_mask: n.srcmacmask.as_ref().map(|v| v.into()), | ||||
|             dst_mac_addr: n.dstmacaddr.as_ref().map(|v| v.into()), | ||||
|             dst_mac_mask: n.dstmacmask.as_ref().map(|v| v.into()), | ||||
|             comment: n.comment.clone(), | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn lib2rest_process_arp_rule(n: &NetworkFilterRuleProtocolArpXML) -> NetworkSelectorARP { | ||||
|         NetworkSelectorARP { | ||||
|             srcmacaddr: n.srcmacaddr.as_ref().map(|v| v.into()), | ||||
|             srcmacmask: n.srcmacmask.as_ref().map(|v| v.into()), | ||||
|             dstmacaddr: n.dstmacaddr.as_ref().map(|v| v.into()), | ||||
|             dstmacmask: n.dstmacmask.as_ref().map(|v| v.into()), | ||||
|             arpsrcipaddr: n.arpsrcipaddr.as_ref().map(|v| v.into()), | ||||
|             arpsrcipmask: n.arpsrcipmask, | ||||
|             arpdstipaddr: n.arpdstipaddr.as_ref().map(|v| v.into()), | ||||
|             arpdstipmask: n.arpdstipmask, | ||||
|             comment: n.comment.clone(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn lib2rest_process_ip_rule<const V: usize>( | ||||
|         n: &NetworkFilterRuleProtocolIpvx, | ||||
|     ) -> NetworkFilterSelectorIP<V> { | ||||
|         NetworkFilterSelectorIP { | ||||
|             srcmacaddr: n.srcmacaddr.as_ref().map(|v| v.into()), | ||||
|             srcmacmask: n.srcmacmask.as_ref().map(|v| v.into()), | ||||
|             dstmacaddr: n.dstmacaddr.as_ref().map(|v| v.into()), | ||||
|             dstmacmask: n.dstmacmask.as_ref().map(|v| v.into()), | ||||
|             srcipaddr: n.srcipaddr.as_ref().map(|v| v.into()), | ||||
|             srcipmask: n.srcipmask, | ||||
|             dstipaddr: n.dstipaddr.as_ref().map(|v| v.into()), | ||||
|             dstipmask: n.dstipmask, | ||||
|             comment: n.comment.clone(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn lib2rest_process_layer4_rule<IPv: Copy>( | ||||
|         n: &NetworkFilterRuleProtocolLayer4<IPv>, | ||||
|     ) -> anyhow::Result<NetworkFilterSelectorLayer4<IPv>> { | ||||
|         Ok(NetworkFilterSelectorLayer4 { | ||||
|             srcmacaddr: n.srcmacaddr.as_ref().map(|v| v.into()), | ||||
|             srcipaddr: n.srcipaddr, | ||||
|             srcipmask: n.srcipmask, | ||||
|             dstipaddr: n.dstipaddr, | ||||
|             dstipmask: n.dstipmask, | ||||
|             srcipfrom: n.srcipfrom, | ||||
|             srcipto: n.srcipto, | ||||
|             dstipfrom: n.dstipfrom, | ||||
|             dstipto: n.dstipto, | ||||
|             srcportstart: n.srcportstart, | ||||
|             srcportend: n.srcportend, | ||||
|             dstportstart: n.dstportstart, | ||||
|             dstportend: n.dstportend, | ||||
|             state: n.state.as_deref().map(Layer4State::from_xml).transpose()?, | ||||
|             comment: n.comment.clone(), | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     pub fn lib2rest(xml: NetworkFilterXML) -> anyhow::Result<Self> { | ||||
|         let mut rules = Vec::with_capacity(xml.rules.len()); | ||||
|         for rule in &xml.rules { | ||||
|             let mut selectors = Vec::new(); | ||||
|  | ||||
|             // All selector | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .all_selectors | ||||
|                     .iter() | ||||
|                     .map(Self::lib2rest_process_all_rule) | ||||
|                     .collect(), | ||||
|             ); | ||||
|  | ||||
|             // Mac rules | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .mac_selectors | ||||
|                     .iter() | ||||
|                     .map(Self::lib2rest_process_mac_rule) | ||||
|                     .collect(), | ||||
|             ); | ||||
|  | ||||
|             // ARP - RARP rules | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .arp_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| NetworkFilterSelector::Arp(Self::lib2rest_process_arp_rule(r))) | ||||
|                     .collect(), | ||||
|             ); | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .rarp_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| NetworkFilterSelector::Rarp(Self::lib2rest_process_arp_rule(r))) | ||||
|                     .collect(), | ||||
|             ); | ||||
|  | ||||
|             // IPv4 - IPv6 rules | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .ipv4_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| NetworkFilterSelector::IPv4(Self::lib2rest_process_ip_rule(r))) | ||||
|                     .collect(), | ||||
|             ); | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .ipv6_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| NetworkFilterSelector::IPv6(Self::lib2rest_process_ip_rule(r))) | ||||
|                     .collect(), | ||||
|             ); | ||||
|  | ||||
|             // Layer 4 protocols | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .tcp_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| { | ||||
|                         Ok(NetworkFilterSelector::TCP( | ||||
|                             Self::lib2rest_process_layer4_rule(r)?, | ||||
|                         )) | ||||
|                     }) | ||||
|                     .collect::<Result<Vec<_>, anyhow::Error>>()?, | ||||
|             ); | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .udp_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| { | ||||
|                         Ok(NetworkFilterSelector::UDP( | ||||
|                             Self::lib2rest_process_layer4_rule(r)?, | ||||
|                         )) | ||||
|                     }) | ||||
|                     .collect::<Result<Vec<_>, anyhow::Error>>()?, | ||||
|             ); | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .sctp_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| { | ||||
|                         Ok(NetworkFilterSelector::SCTP( | ||||
|                             Self::lib2rest_process_layer4_rule(r)?, | ||||
|                         )) | ||||
|                     }) | ||||
|                     .collect::<Result<Vec<_>, anyhow::Error>>()?, | ||||
|             ); | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .icmp_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| { | ||||
|                         Ok(NetworkFilterSelector::ICMP( | ||||
|                             Self::lib2rest_process_layer4_rule(r)?, | ||||
|                         )) | ||||
|                     }) | ||||
|                     .collect::<Result<Vec<_>, anyhow::Error>>()?, | ||||
|             ); | ||||
|  | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .tcp_ipv6_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| { | ||||
|                         Ok(NetworkFilterSelector::TCPipv6( | ||||
|                             Self::lib2rest_process_layer4_rule(r)?, | ||||
|                         )) | ||||
|                     }) | ||||
|                     .collect::<Result<Vec<_>, anyhow::Error>>()?, | ||||
|             ); | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .udp_ipv6_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| { | ||||
|                         Ok(NetworkFilterSelector::UDPipv6( | ||||
|                             Self::lib2rest_process_layer4_rule(r)?, | ||||
|                         )) | ||||
|                     }) | ||||
|                     .collect::<Result<Vec<_>, anyhow::Error>>()?, | ||||
|             ); | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .sctp_ipv6_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| { | ||||
|                         Ok(NetworkFilterSelector::SCTPipv6( | ||||
|                             Self::lib2rest_process_layer4_rule(r)?, | ||||
|                         )) | ||||
|                     }) | ||||
|                     .collect::<Result<Vec<_>, anyhow::Error>>()?, | ||||
|             ); | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .imcp_ipv6_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| { | ||||
|                         Ok(NetworkFilterSelector::ICMPipv6( | ||||
|                             Self::lib2rest_process_layer4_rule(r)?, | ||||
|                         )) | ||||
|                     }) | ||||
|                     .collect::<Result<Vec<_>, anyhow::Error>>()?, | ||||
|             ); | ||||
|  | ||||
|             rules.push(NetworkFilterRule { | ||||
|                 action: NetworkFilterAction::from_xml(&rule.action)?, | ||||
|                 direction: NetworkFilterDirection::from_xml(&rule.direction)?, | ||||
|                 priority: rule.priority, | ||||
|                 selectors, | ||||
|             }) | ||||
|         } | ||||
|  | ||||
|         Ok(Self { | ||||
|             name: NetworkFilterName(xml.name), | ||||
|             uuid: xml.uuid, | ||||
|             chain: xml | ||||
|                 .chain | ||||
|                 .as_deref() | ||||
|                 .map(NetworkFilterChain::from_xml) | ||||
|                 .transpose()?, | ||||
|             priority: xml.priority, | ||||
|             join_filters: xml | ||||
|                 .filterrefs | ||||
|                 .iter() | ||||
|                 .map(|i| NetworkFilterName(i.filter.to_string())) | ||||
|                 .collect(), | ||||
|             rules, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn rest2lib_process_arp_selector( | ||||
|         selector: &NetworkSelectorARP, | ||||
|     ) -> anyhow::Result<NetworkFilterRuleProtocolArpXML> { | ||||
|         Ok(NetworkFilterRuleProtocolArpXML { | ||||
|             srcmacaddr: extract_mac_address_or_var(&selector.srcmacaddr)?, | ||||
|             srcmacmask: extract_mac_address_or_var(&selector.srcmacmask)?, | ||||
|             dstmacaddr: extract_mac_address_or_var(&selector.dstmacaddr)?, | ||||
|             dstmacmask: extract_mac_address_or_var(&selector.dstmacmask)?, | ||||
|             arpsrcipaddr: extract_ip_or_var(&selector.arpsrcipaddr)?, | ||||
|             arpsrcipmask: selector.arpsrcipmask, | ||||
|             arpdstipaddr: extract_ip_or_var(&selector.arpdstipaddr)?, | ||||
|             arpdstipmask: selector.arpdstipmask, | ||||
|             comment: extract_nw_filter_comment(&selector.comment)?, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn rest2lib_process_ip_selector<const V: usize>( | ||||
|         selector: &NetworkFilterSelectorIP<V>, | ||||
|     ) -> anyhow::Result<NetworkFilterRuleProtocolIpvx> { | ||||
|         Ok(NetworkFilterRuleProtocolIpvx { | ||||
|             srcmacaddr: extract_mac_address_or_var(&selector.srcmacaddr)?, | ||||
|             srcmacmask: extract_mac_address_or_var(&selector.srcmacmask)?, | ||||
|             dstmacaddr: extract_mac_address_or_var(&selector.dstmacaddr)?, | ||||
|             dstmacmask: extract_mac_address_or_var(&selector.dstmacmask)?, | ||||
|             srcipaddr: extract_ip_or_var(&selector.srcipaddr)?, | ||||
|             srcipmask: extract_ip_mask::<V>(selector.srcipmask)?, | ||||
|             dstipaddr: extract_ip_or_var(&selector.dstipaddr)?, | ||||
|             dstipmask: extract_ip_mask::<V>(selector.dstipmask)?, | ||||
|             comment: extract_nw_filter_comment(&selector.comment)?, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn rest2lib_process_layer4_selector<IPv: Copy>( | ||||
|         selector: &NetworkFilterSelectorLayer4<IPv>, | ||||
|     ) -> anyhow::Result<NetworkFilterRuleProtocolLayer4<IPv>> { | ||||
|         Ok(NetworkFilterRuleProtocolLayer4 { | ||||
|             srcmacaddr: extract_mac_address_or_var(&selector.srcmacaddr)?, | ||||
|             srcipaddr: selector.srcipaddr, | ||||
|             // This IP mask is not checked | ||||
|             srcipmask: selector.srcipmask, | ||||
|             dstipaddr: selector.dstipaddr, | ||||
|             // This IP mask is not checked | ||||
|             dstipmask: selector.dstipmask, | ||||
|             srcipfrom: selector.srcipfrom, | ||||
|             srcipto: selector.srcipto, | ||||
|             dstipfrom: selector.dstipfrom, | ||||
|             dstipto: selector.dstipto, | ||||
|             srcportstart: selector.srcportstart, | ||||
|             srcportend: selector.srcportend, | ||||
|             dstportstart: selector.dstportstart, | ||||
|             dstportend: selector.dstportend, | ||||
|             state: selector.state.map(|s| s.to_xml()), | ||||
|             comment: extract_nw_filter_comment(&selector.comment)?, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn rest2lib_process_rule(rule: &NetworkFilterRule) -> anyhow::Result<NetworkFilterRuleXML> { | ||||
|         let mut rule_xml = NetworkFilterRuleXML { | ||||
|             action: rule.action.to_xml(), | ||||
|             direction: rule.direction.to_xml(), | ||||
|             priority: rule.priority, | ||||
|             ..Default::default() | ||||
|         }; | ||||
|  | ||||
|         for sel in &rule.selectors { | ||||
|             match sel { | ||||
|                 NetworkFilterSelector::All => { | ||||
|                     rule_xml.all_selectors.push(NetworkFilterRuleProtocolAll {}); | ||||
|                 } | ||||
|  | ||||
|                 NetworkFilterSelector::Mac(mac) => { | ||||
|                     rule_xml.mac_selectors.push(NetworkFilterRuleProtocolMac { | ||||
|                         srcmacaddr: extract_mac_address_or_var(&mac.src_mac_addr)?, | ||||
|                         srcmacmask: extract_mac_address_or_var(&mac.src_mac_mask)?, | ||||
|                         dstmacaddr: extract_mac_address_or_var(&mac.dst_mac_addr)?, | ||||
|                         dstmacmask: extract_mac_address_or_var(&mac.dst_mac_mask)?, | ||||
|                         comment: extract_nw_filter_comment(&mac.comment)?, | ||||
|                     }) | ||||
|                 } | ||||
|  | ||||
|                 NetworkFilterSelector::Arp(a) => { | ||||
|                     rule_xml | ||||
|                         .arp_selectors | ||||
|                         .push(Self::rest2lib_process_arp_selector(a)?); | ||||
|                 } | ||||
|                 NetworkFilterSelector::Rarp(a) => { | ||||
|                     rule_xml | ||||
|                         .rarp_selectors | ||||
|                         .push(Self::rest2lib_process_arp_selector(a)?); | ||||
|                 } | ||||
|  | ||||
|                 NetworkFilterSelector::IPv4(ip) => rule_xml | ||||
|                     .ipv4_selectors | ||||
|                     .push(Self::rest2lib_process_ip_selector(ip)?), | ||||
|  | ||||
|                 NetworkFilterSelector::IPv6(ip) => rule_xml | ||||
|                     .ipv6_selectors | ||||
|                     .push(Self::rest2lib_process_ip_selector(ip)?), | ||||
|  | ||||
|                 NetworkFilterSelector::TCP(tcp) => rule_xml | ||||
|                     .tcp_selectors | ||||
|                     .push(Self::rest2lib_process_layer4_selector(tcp)?), | ||||
|  | ||||
|                 NetworkFilterSelector::UDP(udp) => rule_xml | ||||
|                     .udp_selectors | ||||
|                     .push(Self::rest2lib_process_layer4_selector(udp)?), | ||||
|  | ||||
|                 NetworkFilterSelector::SCTP(sctp) => rule_xml | ||||
|                     .sctp_selectors | ||||
|                     .push(Self::rest2lib_process_layer4_selector(sctp)?), | ||||
|  | ||||
|                 NetworkFilterSelector::ICMP(icmp) => rule_xml | ||||
|                     .icmp_selectors | ||||
|                     .push(Self::rest2lib_process_layer4_selector(icmp)?), | ||||
|  | ||||
|                 NetworkFilterSelector::TCPipv6(tcpv6) => rule_xml | ||||
|                     .tcp_ipv6_selectors | ||||
|                     .push(Self::rest2lib_process_layer4_selector(tcpv6)?), | ||||
|  | ||||
|                 NetworkFilterSelector::UDPipv6(udpv6) => rule_xml | ||||
|                     .udp_ipv6_selectors | ||||
|                     .push(Self::rest2lib_process_layer4_selector(udpv6)?), | ||||
|  | ||||
|                 NetworkFilterSelector::SCTPipv6(sctpv6) => rule_xml | ||||
|                     .sctp_ipv6_selectors | ||||
|                     .push(Self::rest2lib_process_layer4_selector(sctpv6)?), | ||||
|  | ||||
|                 NetworkFilterSelector::ICMPipv6(icmpv6) => rule_xml | ||||
|                     .imcp_ipv6_selectors | ||||
|                     .push(Self::rest2lib_process_layer4_selector(icmpv6)?), | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Ok(rule_xml) | ||||
|     } | ||||
|  | ||||
|     pub fn rest2lib(&self) -> anyhow::Result<NetworkFilterXML> { | ||||
|         if !self.name.is_valid() { | ||||
|             return Err( | ||||
|                 NetworkFilterExtraction("Network filter name is invalid!".to_string()).into(), | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         if let Some(priority) = self.priority { | ||||
|             if !(-1000..=1000).contains(&priority) { | ||||
|                 return Err( | ||||
|                     NetworkFilterExtraction("Network priority is invalid!".to_string()).into(), | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         for fref in &self.join_filters { | ||||
|             if !fref.is_valid() { | ||||
|                 return Err( | ||||
|                     StructureExtraction("Referenced network filter name is invalid!").into(), | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         let mut rules = Vec::with_capacity(self.rules.len()); | ||||
|  | ||||
|         for rule in &self.rules { | ||||
|             rules.push(Self::rest2lib_process_rule(rule)?); | ||||
|         } | ||||
|  | ||||
|         Ok(NetworkFilterXML { | ||||
|             name: self.name.0.to_string(), | ||||
|             uuid: self.uuid, | ||||
|             chain: self.chain.as_ref().map(|c| c.to_xml()), | ||||
|             priority: self.priority, | ||||
|             filterrefs: self | ||||
|                 .join_filters | ||||
|                 .iter() | ||||
|                 .map(|jf| NetworkFilterRefXML { | ||||
|                     filter: jf.0.to_string(), | ||||
|                 }) | ||||
|                 .collect::<Vec<_>>(), | ||||
|             rules, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod test { | ||||
|     use crate::libvirt_rest_structures::nw_filter::is_var_def; | ||||
|  | ||||
|     #[test] | ||||
|     pub fn var_def() { | ||||
|         assert!(is_var_def("$MAC")); | ||||
|         assert!(is_var_def("$MAC_ADDRESS")); | ||||
|  | ||||
|         assert!(!is_var_def("$$MAC")); | ||||
|         assert!(!is_var_def("$$MACé")); | ||||
|         assert!(!is_var_def("$$MAC@")); | ||||
|         assert!(!is_var_def("$$MAC TEST")); | ||||
|     } | ||||
| } | ||||
| @@ -1,56 +1,14 @@ | ||||
| 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, NetIntSourceXML, NetMacAddress, | ||||
|     NetworkBridgeXML, NetworkDHCPHostXML, NetworkDHCPRangeXML, NetworkDHCPXML, | ||||
|     NetworkDNSForwarderXML, NetworkDNSXML, NetworkDomainXML, NetworkForwardXML, NetworkIPXML, | ||||
|     NetworkXML, OSLoaderXML, OSTypeXML, TPMBackendXML, TPMDeviceXML, VideoModelXML, VideoXML, | ||||
|     XMLUuid, ACPIXML, OSXML, | ||||
| }; | ||||
| 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 ipnetwork::{Ipv4Network, Ipv6Network}; | ||||
| use crate::utils::files_utils::convert_size_unit_to_mb; | ||||
| use lazy_regex::regex; | ||||
| use num::Integer; | ||||
| use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; | ||||
| use std::ops::{Div, Mul}; | ||||
| 
 | ||||
| #[derive(thiserror::Error, Debug)] | ||||
| enum LibVirtStructError { | ||||
|     #[error("StructureExtractionError: {0}")] | ||||
|     StructureExtraction(&'static str), | ||||
|     #[error("DomainExtractionError: {0}")] | ||||
|     DomainExtraction(String), | ||||
|     #[error("MBConvertError: {0}")] | ||||
|     MBConvert(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 { | ||||
| @@ -66,11 +24,24 @@ pub enum VMArchitecture { | ||||
|     X86_64, | ||||
| } | ||||
| 
 | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| pub struct NWFilterParam { | ||||
|     name: String, | ||||
|     value: String, | ||||
| } | ||||
| 
 | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| pub struct NWFilterRef { | ||||
|     name: String, | ||||
|     parameters: Vec<NWFilterParam>, | ||||
| } | ||||
| 
 | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| pub struct Network { | ||||
|     mac: String, | ||||
|     #[serde(flatten)] | ||||
|     r#type: NetworkType, | ||||
|     mac: String, | ||||
|     nwfilterref: Option<NWFilterRef>, | ||||
| } | ||||
| 
 | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| @@ -108,7 +79,7 @@ pub struct VMInfo { | ||||
| 
 | ||||
| impl VMInfo { | ||||
|     /// Turn this VM into a domain
 | ||||
|     pub fn to_domain(self) -> anyhow::Result<DomainXML> { | ||||
|     pub fn as_tomain(&self) -> anyhow::Result<DomainXML> { | ||||
|         if !regex!("^[a-zA-Z0-9]+$").is_match(&self.name) { | ||||
|             return Err(StructureExtraction("VM name is invalid!").into()); | ||||
|         } | ||||
| @@ -199,6 +170,67 @@ impl VMInfo { | ||||
|             false => (None, None), | ||||
|         }; | ||||
| 
 | ||||
|         // Process network card
 | ||||
|         let mut networks = vec![]; | ||||
|         for n in &self.networks { | ||||
|             let mac = NetMacAddress { | ||||
|                 address: n.mac.to_string(), | ||||
|             }; | ||||
| 
 | ||||
|             let model = Some(NetIntModelXML { | ||||
|                 r#type: "virtio".to_string(), | ||||
|             }); | ||||
| 
 | ||||
|             let filterref = if let Some(n) = &n.nwfilterref { | ||||
|                 if !regex!("^[a-zA-Z0-9\\_\\-]+$").is_match(&n.name) { | ||||
|                     log::error!("Filter ref name {} is invalid", n.name); | ||||
|                     return Err(StructureExtraction("Network filter ref name is invalid!").into()); | ||||
|                 } | ||||
| 
 | ||||
|                 for p in &n.parameters { | ||||
|                     if !regex!("^[a-zA-Z0-9_-]+$").is_match(&p.name) { | ||||
|                         return Err(StructureExtraction( | ||||
|                             "Network filter ref parameter name is invalid!", | ||||
|                         ) | ||||
|                         .into()); | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 Some(NetIntfilterRefXML { | ||||
|                     filter: n.name.to_string(), | ||||
|                     parameters: n | ||||
|                         .parameters | ||||
|                         .iter() | ||||
|                         .map(|f| NetIntFilterParameterXML { | ||||
|                             name: f.name.to_string(), | ||||
|                             value: f.value.to_string(), | ||||
|                         }) | ||||
|                         .collect(), | ||||
|                 }) | ||||
|             } else { | ||||
|                 None | ||||
|             }; | ||||
| 
 | ||||
|             networks.push(match &n.r#type { | ||||
|                 NetworkType::UserspaceSLIRPStack => DomainNetInterfaceXML { | ||||
|                     mac, | ||||
|                     r#type: "user".to_string(), | ||||
|                     source: None, | ||||
|                     model, | ||||
|                     filterref, | ||||
|                 }, | ||||
|                 NetworkType::DefinedNetwork { network } => DomainNetInterfaceXML { | ||||
|                     mac, | ||||
|                     r#type: "network".to_string(), | ||||
|                     source: Some(NetIntSourceXML { | ||||
|                         network: network.to_string(), | ||||
|                     }), | ||||
|                     model, | ||||
|                     filterref, | ||||
|                 }, | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         // Check disks name for duplicates
 | ||||
|         for disk in &self.disks { | ||||
|             if self.disks.iter().filter(|d| d.name == disk.name).count() > 1 { | ||||
| @@ -206,8 +238,9 @@ impl VMInfo { | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Apply disks configuration
 | ||||
|         for disk in self.disks { | ||||
|         // Apply disks configuration. Starting from now, the function should ideally never fail due to
 | ||||
|         // bad user input
 | ||||
|         for disk in &self.disks { | ||||
|             disk.check_config()?; | ||||
|             disk.apply_config(uuid)?; | ||||
| 
 | ||||
| @@ -241,29 +274,13 @@ impl VMInfo { | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         let mut networks = vec![]; | ||||
|         for n in self.networks { | ||||
|             networks.push(match n.r#type { | ||||
|                 NetworkType::UserspaceSLIRPStack => DomainNetInterfaceXML { | ||||
|                     mac: NetMacAddress { address: n.mac }, | ||||
|                     r#type: "user".to_string(), | ||||
|                     source: None, | ||||
|                 }, | ||||
|                 NetworkType::DefinedNetwork { network } => DomainNetInterfaceXML { | ||||
|                     mac: NetMacAddress { address: n.mac }, | ||||
|                     r#type: "network".to_string(), | ||||
|                     source: Some(NetIntSourceXML { network }), | ||||
|                 }, | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         Ok(DomainXML { | ||||
|             r#type: "kvm".to_string(), | ||||
|             name: self.name, | ||||
|             name: self.name.to_string(), | ||||
|             uuid: Some(uuid), | ||||
|             genid: self.genid.map(|i| i.0), | ||||
|             title: self.title, | ||||
|             description: self.description, | ||||
|             title: self.title.clone(), | ||||
|             description: self.description.clone(), | ||||
| 
 | ||||
|             os: OSXML { | ||||
|                 r#type: OSTypeXML { | ||||
| @@ -370,7 +387,7 @@ impl VMInfo { | ||||
|                 } | ||||
|             }, | ||||
|             number_vcpu: domain.vcpu.body, | ||||
|             memory: convert_to_mb(&domain.memory.unit, domain.memory.memory)?, | ||||
|             memory: convert_size_unit_to_mb(&domain.memory.unit, domain.memory.memory)?, | ||||
|             vnc_access: domain.devices.graphics.is_some(), | ||||
|             iso_files: domain | ||||
|                 .devices | ||||
| @@ -406,6 +423,17 @@ impl VMInfo { | ||||
|                                 ))); | ||||
|                             } | ||||
|                         }, | ||||
|                         nwfilterref: d.filterref.as_ref().map(|f| NWFilterRef { | ||||
|                             name: f.filter.to_string(), | ||||
|                             parameters: f | ||||
|                                 .parameters | ||||
|                                 .iter() | ||||
|                                 .map(|p| NWFilterParam { | ||||
|                                     name: p.name.to_string(), | ||||
|                                     value: p.value.to_string(), | ||||
|                                 }) | ||||
|                                 .collect(), | ||||
|                         }), | ||||
|                     }) | ||||
|                 }) | ||||
|                 .collect::<Result<Vec<_>, _>>()?, | ||||
| @@ -414,309 +442,3 @@ impl VMInfo { | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// Convert unit to MB
 | ||||
| fn convert_to_mb(unit: &str, value: usize) -> anyhow::Result<usize> { | ||||
|     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<DHCPv4HostReservation>, | ||||
| } | ||||
| 
 | ||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] | ||||
| pub struct IPV4Config { | ||||
|     bridge_address: Ipv4Addr, | ||||
|     prefix: u32, | ||||
|     dhcp: Option<IPv4DHCPConfig>, | ||||
| } | ||||
| 
 | ||||
| #[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<DHCPv6HostReservation>, | ||||
| } | ||||
| 
 | ||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] | ||||
| pub struct IPV6Config { | ||||
|     bridge_address: Ipv6Addr, | ||||
|     prefix: u32, | ||||
|     dhcp: Option<IPv6DHCPConfig>, | ||||
| } | ||||
| 
 | ||||
| /// Network configuration
 | ||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] | ||||
| pub struct NetworkInfo { | ||||
|     name: String, | ||||
|     uuid: Option<XMLUuid>, | ||||
|     title: Option<String>, | ||||
|     description: Option<String>, | ||||
|     forward_mode: NetworkForwardMode, | ||||
|     device: Option<String>, | ||||
|     bridge_name: Option<String>, | ||||
|     dns_server: Option<Ipv4Addr>, | ||||
|     domain: Option<String>, | ||||
|     ip_v4: Option<IPV4Config>, | ||||
|     ip_v6: Option<IPV6Config>, | ||||
| } | ||||
| 
 | ||||
| impl NetworkInfo { | ||||
|     pub fn to_virt_network(self) -> anyhow::Result<NetworkXML> { | ||||
|         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.map(|dhcp| NetworkDHCPXML { | ||||
|                     range: NetworkDHCPRangeXML { | ||||
|                         start: IpAddr::V4(dhcp.start), | ||||
|                         end: IpAddr::V4(dhcp.end), | ||||
|                     }, | ||||
|                     hosts: dhcp | ||||
|                         .hosts | ||||
|                         .into_iter() | ||||
|                         .map(|c| NetworkDHCPHostXML { | ||||
|                             mac: c.mac, | ||||
|                             name: c.name, | ||||
|                             ip: c.ip.into(), | ||||
|                         }) | ||||
|                         .collect::<Vec<_>>(), | ||||
|                 }), | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         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.map(|dhcp| NetworkDHCPXML { | ||||
|                     range: NetworkDHCPRangeXML { | ||||
|                         start: IpAddr::V6(dhcp.start), | ||||
|                         end: IpAddr::V6(dhcp.end), | ||||
|                     }, | ||||
|                     hosts: dhcp | ||||
|                         .hosts | ||||
|                         .into_iter() | ||||
|                         .map(|h| NetworkDHCPHostXML { | ||||
|                             mac: "".to_string(), | ||||
|                             name: h.name, | ||||
|                             ip: h.ip.into(), | ||||
|                         }) | ||||
|                         .collect(), | ||||
|                 }), | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         Ok(NetworkXML { | ||||
|             name: self.name, | ||||
|             uuid: self.uuid, | ||||
|             title: self.title, | ||||
|             description: self.description, | ||||
|             forward: match self.forward_mode { | ||||
|                 NetworkForwardMode::NAT => Some(NetworkForwardXML { | ||||
|                     mode: "nat".to_string(), | ||||
|                     dev: self.device.unwrap_or_default(), | ||||
|                 }), | ||||
|                 NetworkForwardMode::Isolated => None, | ||||
|             }, | ||||
|             bridge: self.bridge_name.map(|b| NetworkBridgeXML { | ||||
|                 name: b.to_string(), | ||||
|             }), | ||||
|             dns: self.dns_server.map(|addr| NetworkDNSXML { | ||||
|                 forwarder: NetworkDNSForwarderXML { addr }, | ||||
|             }), | ||||
|             domain: self.domain.map(|name| NetworkDomainXML { name }), | ||||
|             ips, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     pub fn from_xml(xml: NetworkXML) -> anyhow::Result<Self> { | ||||
|         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(), | ||||
|                     }), | ||||
|                 }), | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 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); | ||||
|     } | ||||
| } | ||||
| @@ -22,8 +22,8 @@ use virtweb_backend::constants::{ | ||||
|     MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME, | ||||
| }; | ||||
| use virtweb_backend::controllers::{ | ||||
|     auth_controller, iso_controller, network_controller, server_controller, static_controller, | ||||
|     vm_controller, | ||||
|     auth_controller, iso_controller, network_controller, nwfilter_controller, server_controller, | ||||
|     static_controller, vm_controller, | ||||
| }; | ||||
| use virtweb_backend::libvirt_client::LibVirtClient; | ||||
| use virtweb_backend::middlewares::auth_middleware::AuthChecker; | ||||
| @@ -41,6 +41,7 @@ async fn main() -> std::io::Result<()> { | ||||
|     files_utils::create_directory_if_missing(AppConfig::get().vnc_sockets_path()).unwrap(); | ||||
|     files_utils::set_file_permission(AppConfig::get().vnc_sockets_path(), 0o777).unwrap(); | ||||
|     files_utils::create_directory_if_missing(AppConfig::get().disks_storage_path()).unwrap(); | ||||
|     files_utils::create_directory_if_missing(AppConfig::get().definitions_path()).unwrap(); | ||||
|  | ||||
|     let conn = Data::new(LibVirtClient( | ||||
|         LibVirtActor::connect() | ||||
| @@ -238,6 +239,31 @@ async fn main() -> std::io::Result<()> { | ||||
|                 "/api/network/{uid}/stop", | ||||
|                 web::get().to(network_controller::stop), | ||||
|             ) | ||||
|             // Network filters controller | ||||
|             .route( | ||||
|                 "/api/nwfilter/create", | ||||
|                 web::post().to(nwfilter_controller::create), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/nwfilter/list", | ||||
|                 web::get().to(nwfilter_controller::list), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/nwfilter/{uid}", | ||||
|                 web::get().to(nwfilter_controller::get_single), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/nwfilter/{uid}/src", | ||||
|                 web::get().to(nwfilter_controller::single_src), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/nwfilter/{uid}", | ||||
|                 web::put().to(nwfilter_controller::update), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/nwfilter/{uid}", | ||||
|                 web::delete().to(nwfilter_controller::delete), | ||||
|             ) | ||||
|             // Static assets | ||||
|             .route("/", web::get().to(static_controller::root_index)) | ||||
|             .route( | ||||
|   | ||||
| @@ -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<P: AsRef<Path>>(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<usize> { | ||||
|     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); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
							
								
								
									
										98
									
								
								virtweb_backend/src/utils/net_utils.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								virtweb_backend/src/utils/net_utils.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | ||||
| use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; | ||||
| use std::str::FromStr; | ||||
|  | ||||
| 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, | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn is_ipv4_address_valid<D: AsRef<str>>(ip: D) -> bool { | ||||
|     Ipv4Addr::from_str(ip.as_ref()).is_ok() | ||||
| } | ||||
|  | ||||
| pub fn is_ipv6_address_valid<D: AsRef<str>>(ip: D) -> bool { | ||||
|     Ipv6Addr::from_str(ip.as_ref()).is_ok() | ||||
| } | ||||
|  | ||||
| pub fn is_ipv4_mask_valid(mask: u8) -> bool { | ||||
|     mask <= 32 | ||||
| } | ||||
|  | ||||
| pub fn is_ipv6_mask_valid(mask: u8) -> bool { | ||||
|     mask <= 64 | ||||
| } | ||||
|  | ||||
| pub fn is_mask_valid(ipv: usize, mask: u8) -> bool { | ||||
|     match ipv { | ||||
|         4 => is_ipv4_mask_valid(mask), | ||||
|         6 => is_ipv6_mask_valid(mask), | ||||
|         _ => panic!("Unsupported IP version"), | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn is_mac_address_valid<D: AsRef<str>>(mac: D) -> bool { | ||||
|     lazy_regex::regex!("^([a-fA-F0-9]{2}[:-]){5}[a-fA-F0-9]{2}$").is_match(mac.as_ref()) | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use crate::utils::net_utils::{ | ||||
|         is_ipv4_address_valid, is_ipv6_address_valid, is_mac_address_valid, is_mask_valid, | ||||
|     }; | ||||
|  | ||||
|     #[test] | ||||
|     fn test_is_mac_address_valid() { | ||||
|         assert!(is_mac_address_valid("FF:FF:FF:FF:FF:FF")); | ||||
|         assert!(is_mac_address_valid("02:42:a4:6e:f2:be")); | ||||
|  | ||||
|         assert!(!is_mac_address_valid("tata")); | ||||
|         assert!(!is_mac_address_valid("FF:FF:FF:FF:FF:FZ")); | ||||
|         assert!(!is_mac_address_valid("FF:FF:FF:FF:FF:FF:FF")); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_is_ipv4_address_valid() { | ||||
|         assert!(is_ipv4_address_valid("10.0.0.1")); | ||||
|         assert!(is_ipv4_address_valid("2.56.58.156")); | ||||
|  | ||||
|         assert!(!is_ipv4_address_valid("tata")); | ||||
|         assert!(!is_ipv4_address_valid("1.25.25.288")); | ||||
|         assert!(!is_ipv4_address_valid("5.5.5.5.5")); | ||||
|         assert!(!is_ipv4_address_valid("fe80::")); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_is_ipv6_address_valid() { | ||||
|         assert!(is_ipv6_address_valid("fe80::")); | ||||
|         assert!(is_ipv6_address_valid("fe80:dd::")); | ||||
|         assert!(is_ipv6_address_valid("00:00:00:00:00::")); | ||||
|  | ||||
|         assert!(!is_ipv6_address_valid("tata")); | ||||
|         assert!(!is_ipv6_address_valid("2.56.58.156")); | ||||
|         assert!(!is_ipv6_address_valid("fe::dd::dd")); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_is_mask_valid() { | ||||
|         assert!(is_mask_valid(4, 25)); | ||||
|         assert!(is_mask_valid(4, 32)); | ||||
|         assert!(is_mask_valid(6, 32)); | ||||
|         assert!(is_mask_valid(6, 34)); | ||||
|  | ||||
|         assert!(!is_mask_valid(4, 34)); | ||||
|         assert!(!is_mask_valid(6, 69)); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										23
									
								
								virtweb_docs/SETUP_DEV.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								virtweb_docs/SETUP_DEV.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| # Setup for developpment | ||||
| 1. The `libvirt-dev` package must be installed: | ||||
|  | ||||
| ```bash | ||||
| sudo apt install libvirt-dev | ||||
| ``` | ||||
|  | ||||
| 2. Libvirt must also be installed: | ||||
| ```bash | ||||
| sudo apt install qemu-kvm libvirt-daemon-system | ||||
| ``` | ||||
|  | ||||
| 3. Allow the current user to manage VMs: | ||||
| ``` | ||||
| sudo adduser $USER libvirt | ||||
| sudo adduser $USER kvm  | ||||
| ``` | ||||
|  | ||||
| > Note: You will need to login again for this change to take effect. | ||||
|  | ||||
| 4. Install required developpment tools: | ||||
| * Rust: https://www.rust-lang.org/learn/get-started | ||||
| * NodeJS: https://nodejs.org/en/download/current | ||||
							
								
								
									
										199
									
								
								virtweb_docs/SETUP_PROD.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								virtweb_docs/SETUP_PROD.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,199 @@ | ||||
| # Setup for prod | ||||
|  | ||||
| ## Build VirtWeb for production | ||||
| Open a terminal in the root directory of the VirtWeb project, and run the following command: | ||||
|  | ||||
| ```bash | ||||
| make | ||||
| ``` | ||||
|  | ||||
| The release file will be available in `virtweb_backend/target/release/virtweb_backend`.  | ||||
|  | ||||
| This is the only artifcat that must be copied to the server. It is recommended to copy it to the `/usr/local/bin` directory. | ||||
|  | ||||
| ## Install requirements | ||||
| In order to work properly, VirtWeb relies on `libvirt`, `qemu` and `kvm`: | ||||
|  | ||||
| ```bash | ||||
| sudo apt install qemu-kvm libvirt-daemon-system libvirt0 libvirt-clients libvirt-daemon bridge-utils | ||||
| ``` | ||||
|  | ||||
| ## Dedicated user | ||||
| It is recommended to have a dedicated non-root user to run LibVirt: | ||||
|  | ||||
| ```bash | ||||
| sudo adduser --disabled-login virtweb | ||||
| sudo adduser virtweb libvirt | ||||
| sudo adduser virtweb kvm | ||||
| ``` | ||||
|  | ||||
| When executing this command as this user, it is possible to use the following command:; | ||||
|  | ||||
| ```bash | ||||
| sudo -u virtweb bash | ||||
| ``` | ||||
|  | ||||
| ## Create Virtweb configuration & storage directory | ||||
| Inside the newly created user, create an environment file that will contain the configuration of the VirtWeb software: | ||||
|  | ||||
| ```bash | ||||
| sudo touch /home/virtweb/virtweb-env | ||||
| sudo chmod 600 /home/virtweb/virtweb-env | ||||
| sudo chown virtweb:virtweb /home/virtweb/virtweb-env | ||||
|  | ||||
| sudo mkdir /home/virtweb/storage | ||||
| sudo chown virtweb:kvm /home/virtweb/storage | ||||
|  | ||||
| # Fix storage access permission issue | ||||
| sudo chmod a+rx /home/virtweb | ||||
| ``` | ||||
|  | ||||
| Edit the configuration content: | ||||
|  | ||||
| ```conf | ||||
| LISTEN_ADDRESS=0.0.0.0:8000 | ||||
| WEBSITE_ORIGIN=http://localhost:8000 | ||||
| SECRET=<rand> | ||||
| AUTH_USERNAME=user | ||||
| AUTH_PASSWORD=changeme | ||||
| DISABLE_OIDC=true | ||||
| STORAGE=/home/virtweb/storage | ||||
| HYPERVISOR_URI=qemu:///system | ||||
| ``` | ||||
|  | ||||
| > Note: `HYPERVISOR_URI=qemu:///system` is used to sepcify that we want to use the main hypervisor. | ||||
|  | ||||
| ## Register Virtweb service | ||||
| Before registering service, check that the configuration works correctly: | ||||
|  | ||||
| ```bash | ||||
| sudo -u virtweb virtweb_backend -c /home/virtweb/virtweb-env | ||||
| ``` | ||||
|  | ||||
| Create now a service in the file `/etc/systemd/system/virtweb.service`: | ||||
|  | ||||
| ```conf | ||||
| [Unit] | ||||
| Description=VirtWeb | ||||
| After=syslog.target | ||||
| After=network.target | ||||
|  | ||||
| [Service] | ||||
| RestartSec=2s | ||||
| Type=simple | ||||
| User=virtweb | ||||
| Group=virtweb | ||||
| WorkingDirectory=/home/virtweb | ||||
| ExecStart=/usr/local/bin/virtweb_backend -c /home/virtweb/virtweb-env | ||||
| Restart=always | ||||
| Environment=USER=virtweb  | ||||
| HOME=/home/virtweb | ||||
|  | ||||
| [Install] | ||||
| WantedBy=multi-user.target | ||||
| ``` | ||||
|  | ||||
| Enable and start the created service: | ||||
|  | ||||
| ```bash | ||||
| sudo systemctl enable virtweb | ||||
| sudo systemctl start virtweb | ||||
| ``` | ||||
|  | ||||
| You should now be able to create VMs! | ||||
|  | ||||
| ### Manual port forwarding without a LibVirt HOOK | ||||
| * Allow ip forwarding in the kernel: edit `/etc/sysctl.conf` and uncomment the following line: | ||||
|  | ||||
| ``` | ||||
| net.ipv4.ip_forward=1 | ||||
| ``` | ||||
|  | ||||
| * To reload `sysctl` without reboot: | ||||
|  | ||||
| ``` | ||||
| sudo sysctl -p /etc/sysctl.conf | ||||
| ``` | ||||
|  | ||||
| * Create the following IPTables rules: | ||||
|  | ||||
| ``` | ||||
| UP_DEV=$(ip a | grep "192.168.1." -B 2 | head -n 1 | cut -d ':' -f 2 | | ||||
|  tr -d ' ') | ||||
| LOCAL_DEV=$(ip a | grep "192.168.25." -B 2 | head -n 1 | cut -d ':' -f 2 | tr -d ' ') | ||||
| echo "$UP_DEV -> $LOCAL_DEV" | ||||
|  | ||||
| GUEST_IP=192.168.25.189 | ||||
| HOST_PORT=8085 | ||||
| GUEST_PORT=8085 | ||||
|  | ||||
| # connections from outside | ||||
| sudo iptables -I FORWARD -o $LOCAL_DEV -d  $GUEST_IP -j ACCEPT | ||||
| sudo iptables -t nat -I PREROUTING -p tcp --dport $HOST_PORT -j DNAT --to $GUEST_IP:$GUEST_PORT | ||||
| ``` | ||||
|  | ||||
| * Theses rules can be persisted using `iptables-save` then, or using a libvirt hook. | ||||
|  | ||||
|  | ||||
| ### Manual port forwarding with a LibVirt HOOK | ||||
| * Allow ip forwarding in the kernel: edit `/etc/sysctl.conf` and uncomment the following line: | ||||
|  | ||||
| ``` | ||||
| net.ipv4.ip_forward=1 | ||||
| ``` | ||||
|  | ||||
| * To reload `sysctl` without reboot: | ||||
|  | ||||
| ``` | ||||
| sudo sysctl -p /etc/sysctl.conf | ||||
| ``` | ||||
|  | ||||
| * Get the following information, using the web ui or `virsh`: | ||||
| 	* The name of the target guest | ||||
| 	* The IP and port of the guest who will receive the connection | ||||
| 	* The port of the host that will be forwarded to the guest | ||||
|  | ||||
| * Stop the guest if its running, either using `virsh` or from the web ui | ||||
|  | ||||
| * Create or append the following content to the file `/etc/libvirt/hooks/qemu`: | ||||
|  | ||||
| ```bash | ||||
| #!/bin/bash | ||||
|  | ||||
| # IMPORTANT: Change the "VM NAME" string to match your actual VM Name. | ||||
| # In order to create rules to other VMs, just duplicate the below block and configure | ||||
| # it accordingly. | ||||
| if [ "${1}" = "VM NAME" ]; then | ||||
|  | ||||
|  # Update the following variables to fit your setup | ||||
|  GUEST_IP= | ||||
|  GUEST_PORT= | ||||
|  HOST_PORT= | ||||
|  | ||||
|  if [ "${2}" = "stopped" ] || [ "${2}" = "reconnect" ]; then | ||||
|   /sbin/iptables -D FORWARD -o virbr0 -p tcp -d $GUEST_IP --dport $GUEST_PORT -j ACCEPT | ||||
|   /sbin/iptables -t nat -D PREROUTING -p tcp --dport $HOST_PORT -j DNAT --to $GUEST_IP:$GUEST_PORT | ||||
|  fi | ||||
|  if [ "${2}" = "start" ] || [ "${2}" = "reconnect" ]; then | ||||
|   /sbin/iptables -I FORWARD -o virbr0 -p tcp -d $GUEST_IP --dport $GUEST_PORT -j ACCEPT | ||||
|   /sbin/iptables -t nat -I PREROUTING -p tcp --dport $HOST_PORT -j DNAT --to $GUEST_IP:$GUEST_PORT | ||||
|  fi | ||||
| fi | ||||
| ``` | ||||
|  | ||||
| * Make the hook executable: | ||||
|  | ||||
| ```bash | ||||
| sudo chmod +x /etc/libvirt/hooks/qemu | ||||
| ``` | ||||
|  | ||||
| * Restart the `libvirtd` service: | ||||
|  | ||||
| ```bash | ||||
| sudo systemctl restart libvirtd.service | ||||
| ``` | ||||
|  | ||||
| * Start the guest | ||||
|  | ||||
|  | ||||
| > Note: this guide is based on https://wiki.libvirt.org/Networking.html | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 2.1 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 8.3 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 14 KiB | 
| @@ -25,8 +25,6 @@ import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute"; | ||||
| import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage"; | ||||
| import { BaseLoginPage } from "./widgets/BaseLoginPage"; | ||||
| import { ViewNetworkRoute } from "./routes/ViewNetworkRoute"; | ||||
| import { VMXMLRoute } from "./routes/VMXMLRoute"; | ||||
| import { NetXMLRoute } from "./routes/NetXMLRoute"; | ||||
| import { HomeRoute } from "./routes/HomeRoute"; | ||||
|  | ||||
| interface AuthContext { | ||||
| @@ -57,13 +55,11 @@ export function App() { | ||||
|           <Route path="vm/:uuid" element={<VMRoute />} /> | ||||
|           <Route path="vm/:uuid/edit" element={<EditVMRoute />} /> | ||||
|           <Route path="vm/:uuid/vnc" element={<VNCRoute />} /> | ||||
|           <Route path="vm/:uuid/xml" element={<VMXMLRoute />} /> | ||||
|  | ||||
|           <Route path="net" element={<NetworksListRoute />} /> | ||||
|           <Route path="net/new" element={<CreateNetworkRoute />} /> | ||||
|           <Route path="net/:uuid" element={<ViewNetworkRoute />} /> | ||||
|           <Route path="net/:uuid/edit" element={<EditNetworkRoute />} /> | ||||
|           <Route path="net/:uuid/xml" element={<NetXMLRoute />} /> | ||||
|  | ||||
|           <Route path="sysinfo" element={<SysInfoRoute />} /> | ||||
|           <Route path="*" element={<NotFoundRoute />} /> | ||||
|   | ||||
							
								
								
									
										56
									
								
								virtweb_frontend/src/api/NWFilterApi.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								virtweb_frontend/src/api/NWFilterApi.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| import { APIClient } from "./ApiClient"; | ||||
|  | ||||
| export interface NWFilterChain { | ||||
|   protocol: string; | ||||
|   suffix?: string; | ||||
| } | ||||
|  | ||||
| export interface NWFSAll { | ||||
|   type: "all"; | ||||
| } | ||||
|  | ||||
| export interface NWFSMac { | ||||
|   type: "mac"; | ||||
|   src_mac_addr?: string; | ||||
|   src_mac_mask?: string; | ||||
|   dst_mac_addr?: string; | ||||
|   dst_mac_mask?: string; | ||||
|   comment?: string; | ||||
| } | ||||
|  | ||||
| // TODO : complete | ||||
| export type NWFSelector = NWFSAll | NWFSMac; | ||||
|  | ||||
| export interface NWFilterRule { | ||||
|   action: "drop" | "reject" | "accept" | "return" | "continue"; | ||||
|   direction: "in" | "out" | "inout"; | ||||
|   priority?: number; | ||||
|   selectors: NWFSelector[]; | ||||
| } | ||||
|  | ||||
| export interface NWFilter { | ||||
|   name: string; | ||||
|   chain?: NWFilterChain; | ||||
|   priority?: number; | ||||
|   uuid?: string; | ||||
|   join_filters: string[]; | ||||
|   rules: NWFilterRule[]; | ||||
| } | ||||
|  | ||||
| export class NWFilterApi { | ||||
|   /** | ||||
|    * Get the entire list of networks | ||||
|    */ | ||||
|   static async GetList(): Promise<NWFilter[]> { | ||||
|     const list: NWFilter[] = ( | ||||
|       await APIClient.exec({ | ||||
|         method: "GET", | ||||
|         uri: "/nwfilter/list", | ||||
|       }) | ||||
|     ).data; | ||||
|  | ||||
|     list.sort((a, b) => a.name.localeCompare(b.name)); | ||||
|  | ||||
|     return list; | ||||
|   } | ||||
| } | ||||
| @@ -30,16 +30,30 @@ export interface VMDisk { | ||||
|   deleteType?: "keepfile" | "deletefile"; | ||||
| } | ||||
|  | ||||
| export type VMNetInterface = VMNetUserspaceSLIRPStack | VMNetDefinedNetwork; | ||||
| export interface VMNetInterfaceFilterParams { | ||||
|   name: string; | ||||
|   value: string; | ||||
| } | ||||
|  | ||||
| export interface VMNetInterfaceFilter { | ||||
|   name: string; | ||||
|   parameters: VMNetInterfaceFilterParams[]; | ||||
| } | ||||
|  | ||||
| export type VMNetInterface = (VMNetUserspaceSLIRPStack | VMNetDefinedNetwork) & | ||||
|   VMNetInterfaceBase; | ||||
|  | ||||
| export interface VMNetInterfaceBase { | ||||
|   mac: string; | ||||
|   nwfilterref?: VMNetInterfaceFilter; | ||||
| } | ||||
|  | ||||
| export interface VMNetUserspaceSLIRPStack { | ||||
|   type: "UserspaceSLIRPStack"; | ||||
|   mac: string; | ||||
| } | ||||
|  | ||||
| export interface VMNetDefinedNetwork { | ||||
|   type: "DefinedNetwork"; | ||||
|   mac: string; | ||||
|   network: string; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,19 +1,21 @@ | ||||
| import { Button } from "@mui/material"; | ||||
| import React from "react"; | ||||
| import { useNavigate, useParams } from "react-router-dom"; | ||||
| import { NetworkApi, NetworkInfo, NetworkURL } from "../api/NetworksApi"; | ||||
| import { useAlert } from "../hooks/providers/AlertDialogProvider"; | ||||
| import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider"; | ||||
| import { useSnackbar } from "../hooks/providers/SnackbarProvider"; | ||||
| import React from "react"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { NetworkDetails } from "../widgets/net/NetworkDetails"; | ||||
| import { ConfigImportExportButtons } from "../widgets/ConfigImportExportButtons"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
| import { Button } from "@mui/material"; | ||||
| import { NetworkDetails } from "../widgets/net/NetworkDetails"; | ||||
|  | ||||
| export function CreateNetworkRoute(): React.ReactElement { | ||||
|   const alert = useAlert(); | ||||
|   const snackbar = useSnackbar(); | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const [network] = React.useState<NetworkInfo>({ | ||||
|   const [network, setNetwork] = React.useState<NetworkInfo>({ | ||||
|     name: "NewNetwork", | ||||
|     forward_mode: "Isolated", | ||||
|   }); | ||||
| @@ -35,6 +37,7 @@ export function CreateNetworkRoute(): React.ReactElement { | ||||
|       creating={true} | ||||
|       onCancel={() => navigate("/net")} | ||||
|       onSave={createNetwork} | ||||
|       onReplace={setNetwork} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| @@ -75,6 +78,7 @@ export function EditNetworkRoute(): React.ReactElement { | ||||
|           creating={false} | ||||
|           onCancel={() => navigate(`/net/${uuid}`)} | ||||
|           onSave={updateNetwork} | ||||
|           onReplace={setNetwork} | ||||
|         /> | ||||
|       )} | ||||
|     /> | ||||
| @@ -86,7 +90,10 @@ function EditNetworkRouteInner(p: { | ||||
|   creating: boolean; | ||||
|   onCancel: () => void; | ||||
|   onSave: (vm: NetworkInfo) => Promise<void>; | ||||
|   onReplace: (vm: NetworkInfo) => void; | ||||
| }): React.ReactElement { | ||||
|   const loadingMessage = useLoadingMessage(); | ||||
|  | ||||
|   const [changed, setChanged] = React.useState(false); | ||||
|  | ||||
|   const [, updateState] = React.useState<any>(); | ||||
| @@ -96,15 +103,31 @@ function EditNetworkRouteInner(p: { | ||||
|     setChanged(true); | ||||
|     forceUpdate(); | ||||
|   }; | ||||
|  | ||||
|   const save = async () => { | ||||
|     loadingMessage.show("Saving network configuration..."); | ||||
|     await p.onSave(p.network); | ||||
|     loadingMessage.hide(); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <VirtWebRouteContainer | ||||
|       label={p.creating ? "Create a Network" : "Edit Network"} | ||||
|       actions={ | ||||
|         <span> | ||||
|           <ConfigImportExportButtons | ||||
|             currentConf={p.network} | ||||
|             filename={`net-${p.network.name}.json`} | ||||
|             importConf={(c) => { | ||||
|               p.onReplace(c); | ||||
|               valueChanged(); | ||||
|             }} | ||||
|           /> | ||||
|  | ||||
|           {changed && ( | ||||
|             <Button | ||||
|               variant="contained" | ||||
|               onClick={() => p.onSave(p.network)} | ||||
|               onClick={save} | ||||
|               style={{ marginRight: "10px" }} | ||||
|             > | ||||
|               {p.creating ? "Create" : "Save"} | ||||
|   | ||||
| @@ -5,15 +5,17 @@ import { VMApi, VMInfo } from "../api/VMApi"; | ||||
| import { useAlert } from "../hooks/providers/AlertDialogProvider"; | ||||
| import { useSnackbar } from "../hooks/providers/SnackbarProvider"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { ConfigImportExportButtons } from "../widgets/ConfigImportExportButtons"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
| import { VMDetails } from "../widgets/vms/VMDetails"; | ||||
| import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider"; | ||||
|  | ||||
| export function CreateVMRoute(): React.ReactElement { | ||||
|   const snackbar = useSnackbar(); | ||||
|   const alert = useAlert(); | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const [vm] = React.useState(VMInfo.NewEmpty); | ||||
|   const [vm, setVM] = React.useState(VMInfo.NewEmpty); | ||||
|  | ||||
|   const create = async (v: VMInfo) => { | ||||
|     try { | ||||
| @@ -30,6 +32,7 @@ export function CreateVMRoute(): React.ReactElement { | ||||
|   return ( | ||||
|     <EditVMInner | ||||
|       vm={vm} | ||||
|       onReplace={setVM} | ||||
|       isCreating={true} | ||||
|       onSave={create} | ||||
|       onCancel={() => navigate("/vms")} | ||||
| @@ -64,7 +67,7 @@ export function EditVMRoute(): React.ReactElement { | ||||
|       navigate(v.ViewURL); | ||||
|     } catch (e) { | ||||
|       console.error(e); | ||||
|       alert("Failed to update VM info!"); | ||||
|       alert(`Failed to update VM info!\n${e}`); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
| @@ -76,6 +79,7 @@ export function EditVMRoute(): React.ReactElement { | ||||
|       build={() => ( | ||||
|         <EditVMInner | ||||
|           vm={vm!} | ||||
|           onReplace={setVM} | ||||
|           isCreating={false} | ||||
|           onCancel={() => { | ||||
|             navigate(vm!.ViewURL); | ||||
| @@ -92,7 +96,10 @@ function EditVMInner(p: { | ||||
|   isCreating: boolean; | ||||
|   onCancel: () => void; | ||||
|   onSave: (vm: VMInfo) => Promise<void>; | ||||
|   onReplace: (vm: VMInfo) => void; | ||||
| }): React.ReactElement { | ||||
|   const loadingMessage = useLoadingMessage(); | ||||
|  | ||||
|   const [changed, setChanged] = React.useState(false); | ||||
|  | ||||
|   const [, updateState] = React.useState<any>(); | ||||
| @@ -102,15 +109,30 @@ function EditVMInner(p: { | ||||
|     setChanged(true); | ||||
|     forceUpdate(); | ||||
|   }; | ||||
|  | ||||
|   const save = async () => { | ||||
|     loadingMessage.show("Saving VM configuration..."); | ||||
|     await p.onSave(p.vm); | ||||
|     loadingMessage.hide(); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <VirtWebRouteContainer | ||||
|       label={p.isCreating ? "Create a Virtual Machine" : "Edit Virtual Machine"} | ||||
|       actions={ | ||||
|         <span> | ||||
|           <ConfigImportExportButtons | ||||
|             filename={`vm-${p.vm.name}.json`} | ||||
|             currentConf={p.vm} | ||||
|             importConf={(conf) => { | ||||
|               p.onReplace(new VMInfo(conf)); | ||||
|               valueChanged(); | ||||
|             }} | ||||
|           /> | ||||
|           {changed && ( | ||||
|             <Button | ||||
|               variant="contained" | ||||
|               onClick={() => p.onSave(p.vm)} | ||||
|               onClick={save} | ||||
|               style={{ marginRight: "10px" }} | ||||
|             > | ||||
|               {p.isCreating ? "Create" : "Save"} | ||||
|   | ||||
| @@ -1,50 +0,0 @@ | ||||
| import ArrowBackIcon from "@mui/icons-material/ArrowBack"; | ||||
| import { IconButton } from "@mui/material"; | ||||
| import React from "react"; | ||||
| import { useParams } from "react-router-dom"; | ||||
| import { NetworkApi, NetworkInfo, NetworkURL } from "../api/NetworksApi"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { RouterLink } from "../widgets/RouterLink"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
| import { XMLWidget } from "../widgets/XMLWidget"; | ||||
|  | ||||
| export function NetXMLRoute(): React.ReactElement { | ||||
|   const { uuid } = useParams(); | ||||
|  | ||||
|   const [net, setNet] = React.useState<NetworkInfo | undefined>(); | ||||
|   const [src, setSrc] = React.useState<string | undefined>(); | ||||
|  | ||||
|   const load = async () => { | ||||
|     setNet(await NetworkApi.GetSingle(uuid!)); | ||||
|     setSrc(await NetworkApi.GetSingleXML(uuid!)); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <AsyncWidget | ||||
|       loadKey={uuid} | ||||
|       load={load} | ||||
|       errMsg="Failed to load network information!" | ||||
|       build={() => <XMLRouteInner net={net!} src={src!} />} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function XMLRouteInner(p: { | ||||
|   net: NetworkInfo; | ||||
|   src: string; | ||||
| }): React.ReactElement { | ||||
|   return ( | ||||
|     <VirtWebRouteContainer | ||||
|       label={`XML definition of ${p.net.name}`} | ||||
|       actions={ | ||||
|         <RouterLink to={NetworkURL(p.net)}> | ||||
|           <IconButton> | ||||
|             <ArrowBackIcon /> | ||||
|           </IconButton> | ||||
|         </RouterLink> | ||||
|       } | ||||
|     > | ||||
|       <XMLWidget src={p.src} /> | ||||
|     </VirtWebRouteContainer> | ||||
|   ); | ||||
| } | ||||
| @@ -17,66 +17,31 @@ import { NetworkApi, NetworkInfo, NetworkURL } from "../api/NetworksApi"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { RouterLink } from "../widgets/RouterLink"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
| import { useConfirm } from "../hooks/providers/ConfirmDialogProvider"; | ||||
| import { useSnackbar } from "../hooks/providers/SnackbarProvider"; | ||||
| import { useAlert } from "../hooks/providers/AlertDialogProvider"; | ||||
| import { NetworkStatusWidget } from "../widgets/net/NetworkStatusWidget"; | ||||
| import { useNavigate } from "react-router-dom"; | ||||
|  | ||||
| export function NetworksListRoute(): React.ReactElement { | ||||
|   const confirm = useConfirm(); | ||||
|   const snackbar = useSnackbar(); | ||||
|   const alert = useAlert(); | ||||
|  | ||||
|   const [list, setList] = React.useState<NetworkInfo[] | undefined>(); | ||||
|  | ||||
|   const [count, setCount] = React.useState(1); | ||||
|   const [count] = React.useState(1); | ||||
|  | ||||
|   const load = async () => { | ||||
|     setList(await NetworkApi.GetList()); | ||||
|   }; | ||||
|  | ||||
|   const reload = () => { | ||||
|     setList(undefined); | ||||
|     setCount(count + 1); | ||||
|   }; | ||||
|  | ||||
|   const requestDelete = async (n: NetworkInfo) => { | ||||
|     try { | ||||
|       if ( | ||||
|         !(await confirm( | ||||
|           "Do you really want to delete this network?", | ||||
|           `Delete network ${n.name}`, | ||||
|           "Delete" | ||||
|         )) | ||||
|       ) | ||||
|         return; | ||||
|  | ||||
|       await NetworkApi.Delete(n); | ||||
|       reload(); | ||||
|       snackbar("The network was successfully deleted!"); | ||||
|     } catch (e) { | ||||
|       console.error(e); | ||||
|       alert(`Failed to delete the network!\n${e}`); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <AsyncWidget | ||||
|       loadKey={count} | ||||
|       load={load} | ||||
|       ready={list !== undefined} | ||||
|       errMsg="Failed to load the list of networks!" | ||||
|       build={() => ( | ||||
|         <NetworksListRouteInner list={list!} onRequestDelete={requestDelete} /> | ||||
|       )} | ||||
|       build={() => <NetworksListRouteInner list={list!} />} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NetworksListRouteInner(p: { | ||||
|   list: NetworkInfo[]; | ||||
|   onRequestDelete: (n: NetworkInfo) => void; | ||||
| }): React.ReactElement { | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
| @@ -130,9 +95,6 @@ function NetworksListRouteInner(p: { | ||||
|                         <VisibilityIcon /> | ||||
|                       </IconButton> | ||||
|                     </RouterLink> | ||||
|                     <IconButton onClick={() => p.onRequestDelete(t)}> | ||||
|                       <DeleteIcon /> | ||||
|                     </IconButton> | ||||
|                   </TableCell> | ||||
|                 </TableRow> | ||||
|               ); | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import DeleteIcon from "@mui/icons-material/Delete"; | ||||
| import VisibilityIcon from "@mui/icons-material/Visibility"; | ||||
| import { | ||||
|   Button, | ||||
| @@ -14,15 +13,12 @@ import { | ||||
| } from "@mui/material"; | ||||
| import { filesize } from "filesize"; | ||||
| import React from "react"; | ||||
| import { useNavigate } from "react-router-dom"; | ||||
| import { VMApi, VMInfo } from "../api/VMApi"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { RouterLink } from "../widgets/RouterLink"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
| import { VMStatusWidget } from "../widgets/vms/VMStatusWidget"; | ||||
| import { useSnackbar } from "../hooks/providers/SnackbarProvider"; | ||||
| import { useConfirm } from "../hooks/providers/ConfirmDialogProvider"; | ||||
| import { useNavigate } from "react-router-dom"; | ||||
| import { useAlert } from "../hooks/providers/AlertDialogProvider"; | ||||
|  | ||||
| export function VMListRoute(): React.ReactElement { | ||||
|   const [list, setList] = React.useState<VMInfo[] | undefined>(); | ||||
| @@ -66,39 +62,8 @@ function VMListWidget(p: { | ||||
|   list: VMInfo[]; | ||||
|   onReload: () => void; | ||||
| }): React.ReactElement { | ||||
|   const confirm = useConfirm(); | ||||
|   const alert = useAlert(); | ||||
|   const snackbar = useSnackbar(); | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const deleteVM = async (v: VMInfo) => { | ||||
|     try { | ||||
|       if ( | ||||
|         !(await confirm( | ||||
|           `Do you really want to delete the vm ${v.name}? The operation CANNOT be undone!`, | ||||
|           "Delete a VM", | ||||
|           "DELETE" | ||||
|         )) | ||||
|       ) | ||||
|         return; | ||||
|  | ||||
|       const keepData = !(await confirm( | ||||
|         "Do you want to delete the files of the VM?", | ||||
|         "Delete a VM", | ||||
|         "Delete the data", | ||||
|         "keep the data" | ||||
|       )); | ||||
|  | ||||
|       await VMApi.Delete(v, keepData); | ||||
|       snackbar("The VM was successfully deleted!"); | ||||
|  | ||||
|       p.onReload(); | ||||
|     } catch (e) { | ||||
|       console.error(e); | ||||
|       alert(`Failed to delete VM!\n${e}`); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <TableContainer component={Paper}> | ||||
|       <Table> | ||||
| @@ -135,11 +100,6 @@ function VMListWidget(p: { | ||||
|                     </IconButton> | ||||
|                   </RouterLink> | ||||
|                 </Tooltip> | ||||
|                 <Tooltip title="Delete this VM"> | ||||
|                   <IconButton onClick={() => deleteVM(row)}> | ||||
|                     <DeleteIcon /> | ||||
|                   </IconButton> | ||||
|                 </Tooltip> | ||||
|               </TableCell> | ||||
|             </TableRow> | ||||
|           ))} | ||||
|   | ||||
| @@ -1,14 +1,12 @@ | ||||
| import { Button } from "@mui/material"; | ||||
| import React from "react"; | ||||
| import { useNavigate, useParams } from "react-router-dom"; | ||||
| import { VMApi, VMInfo, VMState } from "../api/VMApi"; | ||||
| import React from "react"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { ConfigImportExportButtons } from "../widgets/ConfigImportExportButtons"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
| import { VMDetails } from "../widgets/vms/VMDetails"; | ||||
| import { VMStatusWidget } from "../widgets/vms/VMStatusWidget"; | ||||
| import { Button, IconButton } from "@mui/material"; | ||||
| import Icon from "@mdi/react"; | ||||
| import { mdiXml } from "@mdi/js"; | ||||
| import { RouterLink } from "../widgets/RouterLink"; | ||||
|  | ||||
| export function VMRoute(): React.ReactElement { | ||||
|   const { uuid } = useParams(); | ||||
| @@ -41,11 +39,10 @@ function VMRouteBody(p: { vm: VMInfo }): React.ReactElement { | ||||
|         <span style={{ display: "inline-flex", alignItems: "center" }}> | ||||
|           <VMStatusWidget vm={p.vm} onChange={setState} /> | ||||
|  | ||||
|           <RouterLink to={p.vm.XMLURL}> | ||||
|             <IconButton size="small"> | ||||
|               <Icon path={mdiXml} style={{ width: "1em" }} /> | ||||
|             </IconButton> | ||||
|           </RouterLink> | ||||
|           <ConfigImportExportButtons | ||||
|             filename={`vm-${p.vm.name}.json`} | ||||
|             currentConf={p.vm} | ||||
|           /> | ||||
|  | ||||
|           {(state === "Shutdown" || state === "Shutoff") && ( | ||||
|             <Button | ||||
|   | ||||
| @@ -1,47 +0,0 @@ | ||||
| import React from "react"; | ||||
| import { useParams } from "react-router-dom"; | ||||
| import { VMApi, VMInfo } from "../api/VMApi"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
| import { IconButton } from "@mui/material"; | ||||
| import { RouterLink } from "../widgets/RouterLink"; | ||||
| import ArrowBackIcon from "@mui/icons-material/ArrowBack"; | ||||
| import { XMLWidget } from "../widgets/XMLWidget"; | ||||
|  | ||||
| export function VMXMLRoute(): React.ReactElement { | ||||
|   const { uuid } = useParams(); | ||||
|  | ||||
|   const [vm, setVM] = React.useState<VMInfo | undefined>(); | ||||
|   const [src, setSrc] = React.useState<string | undefined>(); | ||||
|  | ||||
|   const load = async () => { | ||||
|     setVM(await VMApi.GetSingle(uuid!)); | ||||
|     setSrc(await VMApi.GetSingleXML(uuid!)); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <AsyncWidget | ||||
|       loadKey={uuid} | ||||
|       load={load} | ||||
|       errMsg="Failed to load VM information!" | ||||
|       build={() => <XMLRouteInner vm={vm!} src={src!} />} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function XMLRouteInner(p: { vm: VMInfo; src: string }): React.ReactElement { | ||||
|   return ( | ||||
|     <VirtWebRouteContainer | ||||
|       label={`XML definition of ${p.vm.name}`} | ||||
|       actions={ | ||||
|         <RouterLink to={p.vm.ViewURL}> | ||||
|           <IconButton> | ||||
|             <ArrowBackIcon /> | ||||
|           </IconButton> | ||||
|         </RouterLink> | ||||
|       } | ||||
|     > | ||||
|       <XMLWidget src={p.src} /> | ||||
|     </VirtWebRouteContainer> | ||||
|   ); | ||||
| } | ||||
| @@ -1,6 +1,4 @@ | ||||
| import { mdiXml } from "@mdi/js"; | ||||
| import Icon from "@mdi/react"; | ||||
| import { Button, IconButton } from "@mui/material"; | ||||
| import { Button } from "@mui/material"; | ||||
| import React from "react"; | ||||
| import { useNavigate, useParams } from "react-router-dom"; | ||||
| import { | ||||
| @@ -8,10 +6,9 @@ import { | ||||
|   NetworkInfo, | ||||
|   NetworkStatus, | ||||
|   NetworkURL, | ||||
|   NetworkXMLURL, | ||||
| } from "../api/NetworksApi"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { RouterLink } from "../widgets/RouterLink"; | ||||
| import { ConfigImportExportButtons } from "../widgets/ConfigImportExportButtons"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
| import { NetworkDetails } from "../widgets/net/NetworkDetails"; | ||||
| import { NetworkStatusWidget } from "../widgets/net/NetworkStatusWidget"; | ||||
| @@ -47,14 +44,13 @@ function ViewNetworkRouteInner(p: { | ||||
|     <VirtWebRouteContainer | ||||
|       label={`Network ${p.network.name}`} | ||||
|       actions={ | ||||
|         <span> | ||||
|         <span style={{ display: "flex", alignItems: "center" }}> | ||||
|           <NetworkStatusWidget net={p.network} onChange={setNetStatus} /> | ||||
|  | ||||
|           <RouterLink to={NetworkXMLURL(p.network)}> | ||||
|             <IconButton size="small"> | ||||
|               <Icon path={mdiXml} style={{ width: "1em" }} /> | ||||
|             </IconButton> | ||||
|           </RouterLink> | ||||
|           <ConfigImportExportButtons | ||||
|             filename={`net-${p.network.name}.json`} | ||||
|             currentConf={p.network} | ||||
|           /> | ||||
|  | ||||
|           {netStatus === "Stopped" && ( | ||||
|             <Button | ||||
|   | ||||
							
								
								
									
										74
									
								
								virtweb_frontend/src/widgets/ConfigImportExportButtons.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								virtweb_frontend/src/widgets/ConfigImportExportButtons.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| import FolderOpenIcon from "@mui/icons-material/FolderOpen"; | ||||
| import IosShareIcon from "@mui/icons-material/IosShare"; | ||||
| import { IconButton, Tooltip } from "@mui/material"; | ||||
| import { useAlert } from "../hooks/providers/AlertDialogProvider"; | ||||
|  | ||||
| export function ConfigImportExportButtons(p: { | ||||
|   filename: string; | ||||
|   currentConf: any; | ||||
|   importConf?: (content: any) => any; | ||||
| }): React.ReactElement { | ||||
|   const alert = useAlert(); | ||||
|  | ||||
|   const exportConf = () => { | ||||
|     const conf = JSON.stringify(p.currentConf); | ||||
|     const blob = new Blob([conf], { type: "application/json" }); | ||||
|  | ||||
|     const a = document.createElement("a"); | ||||
|     a.href = window.URL.createObjectURL(blob); | ||||
|     a.download = p.filename; | ||||
|     document.body.appendChild(a); | ||||
|     a.click(); | ||||
|     document.body.removeChild(a); | ||||
|   }; | ||||
|  | ||||
|   const importConf = async () => { | ||||
|     try { | ||||
|       // Create file element | ||||
|       const fileEl = document.createElement("input"); | ||||
|       fileEl.type = "file"; | ||||
|       fileEl.accept = "application/json"; | ||||
|       fileEl.click(); | ||||
|  | ||||
|       // Wait for a file to be chosen | ||||
|       await new Promise((res, _rej) => | ||||
|         fileEl.addEventListener("change", () => res(null)) | ||||
|       ); | ||||
|  | ||||
|       if ((fileEl.files?.length ?? 0) === 0) return null; | ||||
|  | ||||
|       // Import conf | ||||
|       let file = fileEl.files![0]; | ||||
|       const content = await file.text(); | ||||
|       p.importConf?.(JSON.parse(content)); | ||||
|     } catch (e) { | ||||
|       console.error(e); | ||||
|       alert(`Failed to load config from file!\n${e}`); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <Tooltip title={"Export current config"}> | ||||
|         <IconButton | ||||
|           onClick={exportConf} | ||||
|           size="small" | ||||
|           style={{ paddingBottom: "0px", paddingTop: "0px" }} | ||||
|         > | ||||
|           <IosShareIcon /> | ||||
|         </IconButton> | ||||
|       </Tooltip> | ||||
|       {p.importConf && ( | ||||
|         <Tooltip title={"Import config from file"}> | ||||
|           <IconButton | ||||
|             onClick={importConf} | ||||
|             size="small" | ||||
|             style={{ paddingBottom: "0px", paddingTop: "0px" }} | ||||
|           > | ||||
|             <FolderOpenIcon /> | ||||
|           </IconButton> | ||||
|         </Tooltip> | ||||
|       )} | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										35
									
								
								virtweb_frontend/src/widgets/TabsWidget.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								virtweb_frontend/src/widgets/TabsWidget.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| import { Box, Tab, Tabs } from "@mui/material"; | ||||
|  | ||||
| export interface TabWidgetOption<E> { | ||||
|   label: string; | ||||
|   value: E; | ||||
|   visible: boolean; | ||||
|   color?: string; | ||||
| } | ||||
|  | ||||
| export function TabsWidget<E>(p: { | ||||
|   currTab: E; | ||||
|   options: TabWidgetOption<E>[]; | ||||
|   onTabChange: (v: E) => void; | ||||
| }): React.ReactElement { | ||||
|   const activeOptions = p.options.filter((v) => v.visible); | ||||
|  | ||||
|   const currTabIndex = activeOptions.findIndex((v) => v.value === p.currTab); | ||||
|  | ||||
|   const updateActiveTab = (index: number) => { | ||||
|     p.onTabChange(activeOptions[index].value); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <Box sx={{ borderBottom: 1, borderColor: "divider" }}> | ||||
|       <Tabs | ||||
|         value={currTabIndex} | ||||
|         onChange={(_ev, newVal) => updateActiveTab(newVal)} | ||||
|       > | ||||
|         {activeOptions.map((o, index) => ( | ||||
|           <Tab key={index} label={o.label} style={{ color: o.color }} /> | ||||
|         ))} | ||||
|       </Tabs> | ||||
|     </Box> | ||||
|   ); | ||||
| } | ||||
| @@ -3,6 +3,8 @@ import { dracula } from "react-syntax-highlighter/dist/esm/styles/hljs"; | ||||
| import xmlFormat from "xml-formatter"; | ||||
|  | ||||
| import { Light as SyntaxHighlighter } from "react-syntax-highlighter"; | ||||
| import { AsyncWidget } from "./AsyncWidget"; | ||||
| import React from "react"; | ||||
|  | ||||
| SyntaxHighlighter.registerLanguage("xml", xml); | ||||
|  | ||||
| @@ -19,3 +21,24 @@ export function XMLWidget(p: { src: string }): React.ReactElement { | ||||
|     </SyntaxHighlighter> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export function XMLAsyncWidget(p: { | ||||
|   identifier: string; | ||||
|   load: () => Promise<string>; | ||||
|   errMsg: string; | ||||
| }): React.ReactElement { | ||||
|   const [src, setSrc] = React.useState<string | undefined>(); | ||||
|  | ||||
|   const load = async () => { | ||||
|     setSrc(await p.load()); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <AsyncWidget | ||||
|       errMsg={p.errMsg} | ||||
|       load={load} | ||||
|       loadKey={p.identifier} | ||||
|       build={() => <XMLWidget src={src!} />} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -2,23 +2,27 @@ import { Grid, Paper, Typography } from "@mui/material"; | ||||
| import React, { PropsWithChildren } from "react"; | ||||
|  | ||||
| export function EditSection( | ||||
|   p: { title: string; actions?: React.ReactElement } & PropsWithChildren | ||||
|   p: { title?: string; actions?: React.ReactElement } & PropsWithChildren | ||||
| ): React.ReactElement { | ||||
|   return ( | ||||
|     <Grid item sm={12} md={6}> | ||||
|       <Paper style={{ margin: "10px", padding: "10px" }}> | ||||
|         <span | ||||
|           style={{ | ||||
|             display: "flex", | ||||
|             justifyContent: "space-between", | ||||
|             alignItems: "center", | ||||
|           }} | ||||
|         > | ||||
|           <Typography variant="h5" style={{ marginBottom: "15px" }}> | ||||
|             {p.title} | ||||
|           </Typography> | ||||
|           {p.actions} | ||||
|         </span> | ||||
|         {(p.title || p.actions) && ( | ||||
|           <span | ||||
|             style={{ | ||||
|               display: "flex", | ||||
|               justifyContent: "space-between", | ||||
|               alignItems: "center", | ||||
|             }} | ||||
|           > | ||||
|             {p.title && ( | ||||
|               <Typography variant="h5" style={{ marginBottom: "15px" }}> | ||||
|                 {p.title} | ||||
|               </Typography> | ||||
|             )} | ||||
|             {p.actions} | ||||
|           </span> | ||||
|         )} | ||||
|         {p.children} | ||||
|       </Paper> | ||||
|     </Grid> | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import { LenConstraint } from "../../api/ServerApi"; | ||||
|  * Couple / Member property edition | ||||
|  */ | ||||
| export function TextInput(p: { | ||||
|   label: string; | ||||
|   label?: string; | ||||
|   editable: boolean; | ||||
|   value?: string; | ||||
|   onValueChange?: (newVal: string | undefined) => void; | ||||
| @@ -15,6 +15,7 @@ export function TextInput(p: { | ||||
|   minRows?: number; | ||||
|   maxRows?: number; | ||||
|   type?: React.HTMLInputTypeAttribute; | ||||
|   style?: React.CSSProperties; | ||||
| }): React.ReactElement { | ||||
|   if (!p.editable && (p.value ?? "") === "") return <></>; | ||||
|  | ||||
| @@ -48,7 +49,7 @@ export function TextInput(p: { | ||||
|         type: p.type, | ||||
|       }} | ||||
|       variant={"standard"} | ||||
|       style={{ width: "100%", marginBottom: "15px" }} | ||||
|       style={p.style ?? { width: "100%", marginBottom: "15px" }} | ||||
|       multiline={p.multiline} | ||||
|       minRows={p.minRows} | ||||
|       maxRows={p.maxRows} | ||||
|   | ||||
| @@ -0,0 +1,97 @@ | ||||
| import DeleteIcon from "@mui/icons-material/Delete"; | ||||
| import { | ||||
|   Button, | ||||
|   IconButton, | ||||
|   Paper, | ||||
|   Table, | ||||
|   TableBody, | ||||
|   TableCell, | ||||
|   TableContainer, | ||||
|   TableHead, | ||||
|   TableRow, | ||||
|   Tooltip, | ||||
| } from "@mui/material"; | ||||
| import { VMNetInterfaceFilter } from "../../api/VMApi"; | ||||
| import { TextInput } from "./TextInput"; | ||||
|  | ||||
| export function VMNetworkFilterParameters(p: { | ||||
|   editable: boolean; | ||||
|   filterref: VMNetInterfaceFilter; | ||||
|   onChange?: () => void; | ||||
| }): React.ReactElement { | ||||
|   if (!p.editable && p.filterref.parameters.length === 0) return <></>; | ||||
|  | ||||
|   const addParameter = () => { | ||||
|     p.filterref.parameters.push({ name: "", value: "" }); | ||||
|     p.onChange?.(); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       {p.filterref.parameters.length > 0 && ( | ||||
|         <TableContainer component={Paper}> | ||||
|           <Table size="small" aria-label="nwfilter parameters"> | ||||
|             <TableHead> | ||||
|               <TableRow> | ||||
|                 <TableCell>Name</TableCell> | ||||
|                 <TableCell>Value</TableCell> | ||||
|                 {p.editable && <TableCell></TableCell>} | ||||
|               </TableRow> | ||||
|             </TableHead> | ||||
|             <TableBody> | ||||
|               {p.filterref.parameters.map((row, index) => ( | ||||
|                 <TableRow | ||||
|                   key={index} | ||||
|                   sx={{ "&:last-child td, &:last-child th": { border: 0 } }} | ||||
|                 > | ||||
|                   <TableCell | ||||
|                     component="th" | ||||
|                     scope="row" | ||||
|                     style={{ padding: "0px 5px" }} | ||||
|                   > | ||||
|                     <TextInput | ||||
|                       editable={p.editable} | ||||
|                       value={row.name} | ||||
|                       onValueChange={(v) => { | ||||
|                         row.name = v ?? ""; | ||||
|                         p.onChange?.(); | ||||
|                       }} | ||||
|                     /> | ||||
|                   </TableCell> | ||||
|                   <TableCell scope="row" style={{ padding: "0px 5px" }}> | ||||
|                     <TextInput | ||||
|                       editable={p.editable} | ||||
|                       value={row.value} | ||||
|                       onValueChange={(v) => { | ||||
|                         row.value = v ?? ""; | ||||
|                         p.onChange?.(); | ||||
|                       }} | ||||
|                     /> | ||||
|                   </TableCell> | ||||
|                   {p.editable && ( | ||||
|                     <TableCell style={{ padding: "0px" }}> | ||||
|                       <IconButton | ||||
|                         onClick={() => { | ||||
|                           p.filterref.parameters.splice(index, 1); | ||||
|                           p.onChange?.(); | ||||
|                         }} | ||||
|                       > | ||||
|                         <Tooltip title="Remove parameter"> | ||||
|                           <DeleteIcon /> | ||||
|                         </Tooltip> | ||||
|                       </IconButton> | ||||
|                     </TableCell> | ||||
|                   )} | ||||
|                 </TableRow> | ||||
|               ))} | ||||
|             </TableBody> | ||||
|           </Table> | ||||
|         </TableContainer> | ||||
|       )} | ||||
|  | ||||
|       {p.editable && ( | ||||
|         <Button onClick={addParameter}>Add a filter ref parameter</Button> | ||||
|       )} | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| @@ -4,25 +4,30 @@ import DeleteIcon from "@mui/icons-material/Delete"; | ||||
| import { | ||||
|   Avatar, | ||||
|   Button, | ||||
|   Grid, | ||||
|   IconButton, | ||||
|   ListItem, | ||||
|   ListItemAvatar, | ||||
|   ListItemText, | ||||
|   Tooltip, | ||||
| } from "@mui/material"; | ||||
| import { NWFilter } from "../../api/NWFilterApi"; | ||||
| import { NetworkInfo } from "../../api/NetworksApi"; | ||||
| import { ServerApi } from "../../api/ServerApi"; | ||||
| import { VMInfo, VMNetInterface } from "../../api/VMApi"; | ||||
| import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider"; | ||||
| import { SelectInput } from "./SelectInput"; | ||||
| import { NetworkInfo } from "../../api/NetworksApi"; | ||||
| import { randomMacAddress } from "../../utils/RandUtils"; | ||||
| import { ServerApi } from "../../api/ServerApi"; | ||||
| import { MACInput } from "./MACInput"; | ||||
| import { SelectInput } from "./SelectInput"; | ||||
| import { VMNetworkFilterParameters } from "./VMNetworkFilterParameters"; | ||||
| import { EditSection } from "./EditSection"; | ||||
|  | ||||
| export function VMNetworksList(p: { | ||||
|   vm: VMInfo; | ||||
|   onChange?: () => void; | ||||
|   editable: boolean; | ||||
|   networksList: NetworkInfo[]; | ||||
|   networkFiltersList: NWFilter[]; | ||||
| }): React.ReactElement { | ||||
|   const addNew = () => { | ||||
|     p.vm.networks.push({ | ||||
| @@ -34,22 +39,28 @@ export function VMNetworksList(p: { | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       {/* networks list */} | ||||
|       {p.vm.networks.map((n, num) => ( | ||||
|         <NetworkInfoWidget | ||||
|           key={num} | ||||
|           network={n} | ||||
|           removeFromList={() => { | ||||
|             p.vm.networks.splice(num, 1); | ||||
|             p.onChange?.(); | ||||
|           }} | ||||
|           {...p} | ||||
|         /> | ||||
|       ))} | ||||
|  | ||||
|       {p.editable && ( | ||||
|         <Button onClick={addNew}>Add a new network interface</Button> | ||||
|         <div style={{ textAlign: "right", marginTop: "5px" }}> | ||||
|           <Button onClick={addNew}>Add a new network interface</Button> | ||||
|         </div> | ||||
|       )} | ||||
|  | ||||
|       <Grid container spacing={2}> | ||||
|         {/* networks list */} | ||||
|         {p.vm.networks.map((n, num) => ( | ||||
|           <EditSection key={num}> | ||||
|             <NetworkInfoWidget | ||||
|               key={num} | ||||
|               network={n} | ||||
|               removeFromList={() => { | ||||
|                 p.vm.networks.splice(num, 1); | ||||
|                 p.onChange?.(); | ||||
|               }} | ||||
|               {...p} | ||||
|             /> | ||||
|           </EditSection> | ||||
|         ))} | ||||
|       </Grid> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| @@ -60,6 +71,7 @@ function NetworkInfoWidget(p: { | ||||
|   onChange?: () => void; | ||||
|   removeFromList: () => void; | ||||
|   networksList: NetworkInfo[]; | ||||
|   networkFiltersList: NWFilter[]; | ||||
| }): React.ReactElement { | ||||
|   const confirm = useConfirm(); | ||||
|   const deleteNetwork = async () => { | ||||
| @@ -137,28 +149,66 @@ function NetworkInfoWidget(p: { | ||||
|         /> | ||||
|  | ||||
|         {p.network.type === "DefinedNetwork" && ( | ||||
|           <SelectInput | ||||
|             editable={p.editable} | ||||
|             label="Defined network" | ||||
|             options={p.networksList.map((n) => { | ||||
|               const chars = [n.forward_mode.toString()]; | ||||
|               if (n.ip_v4) chars.push("IPv4"); | ||||
|               if (n.ip_v6) chars.push("IPv6"); | ||||
|               if (n.description) chars.push(n.description); | ||||
|           <> | ||||
|             <SelectInput | ||||
|               editable={p.editable} | ||||
|               label="Defined network" | ||||
|               options={p.networksList.map((n) => { | ||||
|                 const chars = [n.forward_mode.toString()]; | ||||
|                 if (n.ip_v4) chars.push("IPv4"); | ||||
|                 if (n.ip_v6) chars.push("IPv6"); | ||||
|                 if (n.description) chars.push(n.description); | ||||
|  | ||||
|               return { | ||||
|                 label: n.name, | ||||
|                 value: n.name, | ||||
|                 description: chars.join(" - "), | ||||
|               }; | ||||
|             })} | ||||
|             value={p.network.network} | ||||
|             onValueChange={(v) => { | ||||
|               if (p.network.type === "DefinedNetwork") | ||||
|                 p.network.network = v as any; | ||||
|               p.onChange?.(); | ||||
|             }} | ||||
|           /> | ||||
|                 return { | ||||
|                   label: n.name, | ||||
|                   value: n.name, | ||||
|                   description: chars.join(" - "), | ||||
|                 }; | ||||
|               })} | ||||
|               value={p.network.network} | ||||
|               onValueChange={(v) => { | ||||
|                 if (p.network.type === "DefinedNetwork") | ||||
|                   p.network.network = v as any; | ||||
|                 p.onChange?.(); | ||||
|               }} | ||||
|             /> | ||||
|  | ||||
|             {/* Network Filter */} | ||||
|             <SelectInput | ||||
|               editable={p.editable} | ||||
|               label="Network filter" | ||||
|               value={p.network.nwfilterref?.name} | ||||
|               onValueChange={(v) => { | ||||
|                 if (v && !p.network.nwfilterref) { | ||||
|                   p.network.nwfilterref = { name: v, parameters: [] }; | ||||
|                 } else if (v) { | ||||
|                   p.network.nwfilterref!.name = v; | ||||
|                 } else { | ||||
|                   p.network.nwfilterref = undefined; | ||||
|                 } | ||||
|                 p.onChange?.(); | ||||
|               }} | ||||
|               options={[ | ||||
|                 { label: "No network filer", value: undefined }, | ||||
|                 ...p.networkFiltersList.map((v) => { | ||||
|                   return { | ||||
|                     value: v.name, | ||||
|                     label: `${v.name} (${v.chain?.protocol ?? "unspecified"})`, | ||||
|                     description: `${v.rules.length} rules - ${v.join_filters.length} joint filters`, | ||||
|                   }; | ||||
|                 }), | ||||
|               ]} | ||||
|             /> | ||||
|  | ||||
|             {p.network.nwfilterref && ( | ||||
|               <div style={{ margin: "10px" }}> | ||||
|                 <VMNetworkFilterParameters | ||||
|                   filterref={p.network.nwfilterref} | ||||
|                   {...p} | ||||
|                 /> | ||||
|               </div> | ||||
|             )} | ||||
|           </> | ||||
|         )} | ||||
|       </div> | ||||
|     </> | ||||
|   | ||||
| @@ -23,27 +23,6 @@ export function VMSelectIsoInput(p: { | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <SelectInput | ||||
|         label="Attach an ISO file" | ||||
|         editable={p.editable} | ||||
|         value={undefined} | ||||
|         onValueChange={(v) => { | ||||
|           if (v) { | ||||
|             p.attachedISOs.push(v); | ||||
|             p.onChange(p.attachedISOs); | ||||
|           } | ||||
|         }} | ||||
|         options={[ | ||||
|           { label: "None", value: undefined }, | ||||
|           ...p.isoList.map((i) => { | ||||
|             return { | ||||
|               label: `${i.filename} ${filesize(i.size)}`, | ||||
|               value: i.filename, | ||||
|             }; | ||||
|           }), | ||||
|         ]} | ||||
|       /> | ||||
|  | ||||
|       {p.attachedISOs.map((isoName, num) => { | ||||
|         const iso = p.isoList.find((d) => d.filename === isoName); | ||||
|         return ( | ||||
| @@ -78,6 +57,27 @@ export function VMSelectIsoInput(p: { | ||||
|           </ListItem> | ||||
|         ); | ||||
|       })} | ||||
|  | ||||
|       <SelectInput | ||||
|         label="Attach an ISO file" | ||||
|         editable={p.editable} | ||||
|         value={undefined} | ||||
|         onValueChange={(v) => { | ||||
|           if (v) { | ||||
|             p.attachedISOs.push(v); | ||||
|             p.onChange(p.attachedISOs); | ||||
|           } | ||||
|         }} | ||||
|         options={[ | ||||
|           { label: "None", value: undefined }, | ||||
|           ...p.isoList.map((i) => { | ||||
|             return { | ||||
|               label: `${i.filename} ${filesize(i.size)}`, | ||||
|               value: i.filename, | ||||
|             }; | ||||
|           }), | ||||
|         ]} | ||||
|       /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -75,7 +75,7 @@ function HostReservationWidget(p: { | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <Paper elevation={3} style={{ padding: "10px", marginTop: "20px" }}> | ||||
|     <Paper elevation={3} style={{ padding: "20px", marginBottom: "20px" }}> | ||||
|       <ListItem | ||||
|         secondaryAction={ | ||||
|           p.editable && ( | ||||
|   | ||||
| @@ -1,16 +1,20 @@ | ||||
| import { Checkbox, Grid, Paper } from "@mui/material"; | ||||
| import { Button, Checkbox, Grid } from "@mui/material"; | ||||
| import React from "react"; | ||||
| import { useNavigate } from "react-router-dom"; | ||||
| import { IpConfig, NetworkApi, NetworkInfo } from "../../api/NetworksApi"; | ||||
| import { ServerApi } from "../../api/ServerApi"; | ||||
| import { useAlert } from "../../hooks/providers/AlertDialogProvider"; | ||||
| import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider"; | ||||
| import { useSnackbar } from "../../hooks/providers/SnackbarProvider"; | ||||
| import { AsyncWidget } from "../AsyncWidget"; | ||||
| import { TabsWidget } from "../TabsWidget"; | ||||
| import { EditSection } from "../forms/EditSection"; | ||||
| import { IPInput } from "../forms/IPInput"; | ||||
| import { ResAutostartInput } from "../forms/ResAutostartInput"; | ||||
| import { SelectInput } from "../forms/SelectInput"; | ||||
| import { TextInput } from "../forms/TextInput"; | ||||
| import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider"; | ||||
| import { CheckboxInput } from "../forms/CheckboxInput"; | ||||
| import { ResAutostartInput } from "../forms/ResAutostartInput"; | ||||
| import { DHCPHostReservations } from "./DHCPHostReservations"; | ||||
| import { XMLAsyncWidget } from "../XMLWidget"; | ||||
|  | ||||
| interface DetailsProps { | ||||
|   net: NetworkInfo; | ||||
| @@ -35,9 +39,60 @@ export function NetworkDetails(p: DetailsProps): React.ReactElement { | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NetworkDetailsInner( | ||||
|   p: DetailsProps & { cardsList: string[] } | ||||
| ): React.ReactElement { | ||||
| enum NetTab { | ||||
|   General = 0, | ||||
|   IPv4, | ||||
|   IPv6, | ||||
|   XML, | ||||
|   Danger, | ||||
| } | ||||
|  | ||||
| type DetailsInnerProps = DetailsProps & { cardsList: string[] }; | ||||
|  | ||||
| function NetworkDetailsInner(p: DetailsInnerProps): React.ReactElement { | ||||
|   const [currTab, setCurrTab] = React.useState(NetTab.General); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <TabsWidget | ||||
|         currTab={currTab} | ||||
|         onTabChange={setCurrTab} | ||||
|         options={[ | ||||
|           { label: "General", value: NetTab.General, visible: true }, | ||||
|           { | ||||
|             label: "IPv4", | ||||
|             value: NetTab.IPv4, | ||||
|             visible: p.editable || !!p.net.ip_v4, | ||||
|           }, | ||||
|           { | ||||
|             label: "IPv6", | ||||
|             value: NetTab.IPv6, | ||||
|             visible: p.editable || !!p.net.ip_v6, | ||||
|           }, | ||||
|           { | ||||
|             label: "XML", | ||||
|             value: NetTab.XML, | ||||
|             visible: !p.editable, | ||||
|           }, | ||||
|           { | ||||
|             label: "Danger zone", | ||||
|             value: NetTab.Danger, | ||||
|             color: "red", | ||||
|             visible: !p.editable, | ||||
|           }, | ||||
|         ]} | ||||
|       /> | ||||
|  | ||||
|       {currTab === NetTab.General && <NetworkDetailsTabGeneral {...p} />} | ||||
|       {currTab === NetTab.IPv4 && <NetworkDetailsTabIPv4 {...p} />} | ||||
|       {currTab === NetTab.IPv6 && <NetworkDetailsTabIPv6 {...p} />} | ||||
|       {currTab === NetTab.XML && <NetworkDetailsTabXML {...p} />} | ||||
|       {currTab === NetTab.Danger && <NetworkDetailsTabDanger {...p} />} | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NetworkDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement { | ||||
|   return ( | ||||
|     <Grid container spacing={2}> | ||||
|       {/* Metadata section */} | ||||
| @@ -161,30 +216,38 @@ function NetworkDetailsInner( | ||||
|           multiline={true} | ||||
|         /> | ||||
|       </EditSection> | ||||
|  | ||||
|       <IPSection | ||||
|         editable={p.editable} | ||||
|         config={p.net.ip_v4} | ||||
|         onChange={(c) => { | ||||
|           p.net.ip_v4 = c; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|         version={4} | ||||
|       /> | ||||
|  | ||||
|       <IPSection | ||||
|         editable={p.editable} | ||||
|         config={p.net.ip_v6} | ||||
|         onChange={(c) => { | ||||
|           p.net.ip_v6 = c; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|         version={6} | ||||
|       /> | ||||
|     </Grid> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NetworkDetailsTabIPv4(p: DetailsInnerProps): React.ReactElement { | ||||
|   return ( | ||||
|     <IPSection | ||||
|       editable={p.editable} | ||||
|       config={p.net.ip_v4} | ||||
|       onChange={(c) => { | ||||
|         p.net.ip_v4 = c; | ||||
|         p.onChange?.(); | ||||
|       }} | ||||
|       version={4} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NetworkDetailsTabIPv6(p: DetailsInnerProps): React.ReactElement { | ||||
|   return ( | ||||
|     <IPSection | ||||
|       editable={p.editable} | ||||
|       config={p.net.ip_v6} | ||||
|       onChange={(c) => { | ||||
|         p.net.ip_v6 = c; | ||||
|         p.onChange?.(); | ||||
|       }} | ||||
|       version={6} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function IPSection(p: { | ||||
|   editable: boolean; | ||||
|   config?: IpConfig; | ||||
| @@ -212,100 +275,159 @@ function IPSection(p: { | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   const toggleDHCP = (v: boolean) => { | ||||
|     if (v) | ||||
|       p.config!.dhcp = | ||||
|         p.version === 4 | ||||
|           ? { | ||||
|               start: "192.168.1.100", | ||||
|               end: "192.168.1.200", | ||||
|               hosts: [], | ||||
|             } | ||||
|           : { start: "fd00::100", end: "fd00::f00", hosts: [] }; | ||||
|     else p.config!.dhcp = undefined; | ||||
|  | ||||
|     p.onChange?.(p.config); | ||||
|   }; | ||||
|  | ||||
|   if (!p.config && !p.editable) return <></>; | ||||
|  | ||||
|   return ( | ||||
|     <EditSection | ||||
|       title={`IPv${p.version} network`} | ||||
|       actions={ | ||||
|         <Checkbox | ||||
|           disabled={!p.editable} | ||||
|           checked={!!p.config} | ||||
|           onChange={toggleNetwork} | ||||
|         /> | ||||
|       } | ||||
|     > | ||||
|       {p.config && ( | ||||
|         <> | ||||
|           <IPInput | ||||
|             editable={p.editable} | ||||
|             label="Bridge address" | ||||
|             version={p.version} | ||||
|             value={p.config?.bridge_address} | ||||
|             onValueChange={(v) => { | ||||
|               p.config!.bridge_address = v ?? ""; | ||||
|               p.onChange?.(p.config); | ||||
|             }} | ||||
|     <Grid container spacing={2}> | ||||
|       <EditSection | ||||
|         title={`IPv${p.version} network`} | ||||
|         actions={ | ||||
|           <Checkbox | ||||
|             disabled={!p.editable} | ||||
|             checked={!!p.config} | ||||
|             onChange={toggleNetwork} | ||||
|           /> | ||||
|  | ||||
|           <TextInput | ||||
|             label="Prefix" | ||||
|             editable={p.editable} | ||||
|             value={p.config.prefix.toString()} | ||||
|             type="number" | ||||
|             onValueChange={(v) => { | ||||
|               p.config!.prefix = Number(v); | ||||
|               p.onChange?.(p.config); | ||||
|             }} | ||||
|             size={p.version === 4 ? { min: 0, max: 32 } : { min: 0, max: 128 }} | ||||
|           /> | ||||
|  | ||||
|           <CheckboxInput | ||||
|             checked={!!p.config.dhcp} | ||||
|             editable={p.editable} | ||||
|             label="Enable DHCP" | ||||
|             onValueChange={(v) => { | ||||
|               if (v) | ||||
|                 p.config!.dhcp = | ||||
|                   p.version === 4 | ||||
|                     ? { | ||||
|                         start: "192.168.1.100", | ||||
|                         end: "192.168.1.200", | ||||
|                         hosts: [], | ||||
|                       } | ||||
|                     : { start: "fd00::100", end: "fd00::f00", hosts: [] }; | ||||
|               else p.config!.dhcp = undefined; | ||||
|               p.onChange?.(p.config); | ||||
|             }} | ||||
|           /> | ||||
|         </> | ||||
|       )} | ||||
|  | ||||
|       {p.config?.dhcp && ( | ||||
|         <> | ||||
|           <Paper elevation={3} style={{ padding: "10px" }}> | ||||
|         } | ||||
|       > | ||||
|         {p.config && ( | ||||
|           <> | ||||
|             <IPInput | ||||
|               label="DHCP allocation start" | ||||
|               editable={p.editable} | ||||
|               label="Bridge address" | ||||
|               version={p.version} | ||||
|               value={p.config.dhcp.start} | ||||
|               value={p.config?.bridge_address} | ||||
|               onValueChange={(v) => { | ||||
|                 p.config!.dhcp!.start = v!; | ||||
|                 p.onChange(p.config); | ||||
|               }} | ||||
|             /> | ||||
|             <IPInput | ||||
|               label="DHCP allocation end" | ||||
|               editable={p.editable} | ||||
|               version={p.version} | ||||
|               value={p.config.dhcp.end} | ||||
|               onValueChange={(v) => { | ||||
|                 p.config!.dhcp!.end = v!; | ||||
|                 p.onChange(p.config); | ||||
|               }} | ||||
|             /> | ||||
|  | ||||
|             <DHCPHostReservations | ||||
|               {...p} | ||||
|               dhcp={p.config.dhcp} | ||||
|               onChange={(d) => { | ||||
|                 p.config!.dhcp = d; | ||||
|                 p.config!.bridge_address = v ?? ""; | ||||
|                 p.onChange?.(p.config); | ||||
|               }} | ||||
|             /> | ||||
|           </Paper> | ||||
|         </> | ||||
|  | ||||
|             <TextInput | ||||
|               label="Prefix" | ||||
|               editable={p.editable} | ||||
|               value={p.config.prefix.toString()} | ||||
|               type="number" | ||||
|               onValueChange={(v) => { | ||||
|                 p.config!.prefix = Number(v); | ||||
|                 p.onChange?.(p.config); | ||||
|               }} | ||||
|               size={ | ||||
|                 p.version === 4 ? { min: 0, max: 32 } : { min: 0, max: 128 } | ||||
|               } | ||||
|             /> | ||||
|           </> | ||||
|         )} | ||||
|       </EditSection> | ||||
|  | ||||
|       {p.config && (p.editable || p.config.dhcp) && ( | ||||
|         <EditSection | ||||
|           title={`DHCP v${p.version}`} | ||||
|           actions={ | ||||
|             <Checkbox | ||||
|               disabled={!p.editable} | ||||
|               checked={!!p.config.dhcp} | ||||
|               onChange={(_ev, val) => toggleDHCP(val)} | ||||
|             /> | ||||
|           } | ||||
|         > | ||||
|           {p.config.dhcp && ( | ||||
|             <> | ||||
|               <IPInput | ||||
|                 label="DHCP allocation start" | ||||
|                 editable={p.editable} | ||||
|                 version={p.version} | ||||
|                 value={p.config.dhcp.start} | ||||
|                 onValueChange={(v) => { | ||||
|                   p.config!.dhcp!.start = v!; | ||||
|                   p.onChange(p.config); | ||||
|                 }} | ||||
|               /> | ||||
|               <IPInput | ||||
|                 label="DHCP allocation end" | ||||
|                 editable={p.editable} | ||||
|                 version={p.version} | ||||
|                 value={p.config.dhcp.end} | ||||
|                 onValueChange={(v) => { | ||||
|                   p.config!.dhcp!.end = v!; | ||||
|                   p.onChange(p.config); | ||||
|                 }} | ||||
|               /> | ||||
|             </> | ||||
|           )} | ||||
|         </EditSection> | ||||
|       )} | ||||
|     </EditSection> | ||||
|  | ||||
|       {p.config?.dhcp && (p.editable || p.config.dhcp.hosts.length > 0) && ( | ||||
|         <EditSection title="DHCP hosts reservations"> | ||||
|           <DHCPHostReservations | ||||
|             {...p} | ||||
|             dhcp={p.config.dhcp} | ||||
|             onChange={(d) => { | ||||
|               p.config!.dhcp = d; | ||||
|               p.onChange?.(p.config); | ||||
|             }} | ||||
|           /> | ||||
|         </EditSection> | ||||
|       )} | ||||
|     </Grid> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NetworkDetailsTabXML(p: DetailsInnerProps): React.ReactElement { | ||||
|   return ( | ||||
|     <XMLAsyncWidget | ||||
|       errMsg="Failed to load network XML definition!" | ||||
|       identifier={p.net.uuid!} | ||||
|       load={() => NetworkApi.GetSingleXML(p.net.uuid!)} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NetworkDetailsTabDanger(p: DetailsInnerProps): React.ReactElement { | ||||
|   const confirm = useConfirm(); | ||||
|   const snackbar = useSnackbar(); | ||||
|   const alert = useAlert(); | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const requestDelete = async () => { | ||||
|     try { | ||||
|       if ( | ||||
|         !(await confirm( | ||||
|           "Do you really want to delete this network?", | ||||
|           `Delete network ${p.net.name}`, | ||||
|           "Delete" | ||||
|         )) | ||||
|       ) | ||||
|         return; | ||||
|  | ||||
|       await NetworkApi.Delete(p.net); | ||||
|  | ||||
|       navigate("/net"); | ||||
|       snackbar("The network was successfully deleted!"); | ||||
|     } catch (e) { | ||||
|       console.error(e); | ||||
|       alert(`Failed to delete the network!\n${e}`); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <Button color="error" onClick={requestDelete}> | ||||
|       Delete this network | ||||
|     </Button> | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -1,20 +1,27 @@ | ||||
| import { Grid } from "@mui/material"; | ||||
| import { Button, Grid } from "@mui/material"; | ||||
| import React from "react"; | ||||
| import { useNavigate } from "react-router-dom"; | ||||
| import { validate as validateUUID } from "uuid"; | ||||
| import { IsoFile, IsoFilesApi } from "../../api/IsoFilesApi"; | ||||
| import { NWFilter, NWFilterApi } from "../../api/NWFilterApi"; | ||||
| import { NetworkApi, NetworkInfo } from "../../api/NetworksApi"; | ||||
| import { ServerApi } from "../../api/ServerApi"; | ||||
| import { VMApi, VMInfo } from "../../api/VMApi"; | ||||
| import { useAlert } from "../../hooks/providers/AlertDialogProvider"; | ||||
| import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider"; | ||||
| import { useSnackbar } from "../../hooks/providers/SnackbarProvider"; | ||||
| import { AsyncWidget } from "../AsyncWidget"; | ||||
| import { TabsWidget } from "../TabsWidget"; | ||||
| import { CheckboxInput } from "../forms/CheckboxInput"; | ||||
| import { EditSection } from "../forms/EditSection"; | ||||
| import { ResAutostartInput } from "../forms/ResAutostartInput"; | ||||
| import { SelectInput } from "../forms/SelectInput"; | ||||
| import { TextInput } from "../forms/TextInput"; | ||||
| import { VMDisksList } from "../forms/VMDisksList"; | ||||
| import { VMNetworksList } from "../forms/VMNetworksList"; | ||||
| import { VMSelectIsoInput } from "../forms/VMSelectIsoInput"; | ||||
| import { VMScreenshot } from "./VMScreenshot"; | ||||
| import { ResAutostartInput } from "../forms/ResAutostartInput"; | ||||
| import { VMNetworksList } from "../forms/VMNetworksList"; | ||||
| import { NetworkApi, NetworkInfo } from "../../api/NetworksApi"; | ||||
| import { XMLAsyncWidget } from "../XMLWidget"; | ||||
|  | ||||
| interface DetailsProps { | ||||
|   vm: VMInfo; | ||||
| @@ -29,11 +36,15 @@ export function VMDetails(p: DetailsProps): React.ReactElement { | ||||
|     number[] | any | ||||
|   >(); | ||||
|   const [networksList, setNetworksList] = React.useState<NetworkInfo[] | any>(); | ||||
|   const [networkFiltersList, setNetworkFiltersList] = React.useState< | ||||
|     NWFilter[] | any | ||||
|   >(); | ||||
|  | ||||
|   const load = async () => { | ||||
|     setIsoList(await IsoFilesApi.GetList()); | ||||
|     setVCPUCombinations(await ServerApi.NumberVCPUs()); | ||||
|     setNetworksList(await NetworkApi.GetList()); | ||||
|     setNetworkFiltersList(await NWFilterApi.GetList()); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
| @@ -46,6 +57,7 @@ export function VMDetails(p: DetailsProps): React.ReactElement { | ||||
|           isoList={isoList} | ||||
|           vcpuCombinations={vcpuCombinations} | ||||
|           networksList={networksList} | ||||
|           networkFiltersList={networkFiltersList} | ||||
|           {...p} | ||||
|         /> | ||||
|       )} | ||||
| @@ -53,13 +65,57 @@ export function VMDetails(p: DetailsProps): React.ReactElement { | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function VMDetailsInner( | ||||
|   p: DetailsProps & { | ||||
|     isoList: IsoFile[]; | ||||
|     vcpuCombinations: number[]; | ||||
|     networksList: NetworkInfo[]; | ||||
|   } | ||||
| ): React.ReactElement { | ||||
| enum VMTab { | ||||
|   General = 0, | ||||
|   Storage, | ||||
|   Network, | ||||
|   XML, | ||||
|   Danger, | ||||
| } | ||||
|  | ||||
| type DetailsInnerProps = DetailsProps & { | ||||
|   isoList: IsoFile[]; | ||||
|   vcpuCombinations: number[]; | ||||
|   networksList: NetworkInfo[]; | ||||
|   networkFiltersList: NWFilter[]; | ||||
| }; | ||||
|  | ||||
| function VMDetailsInner(p: DetailsInnerProps): React.ReactElement { | ||||
|   const [currTab, setCurrTab] = React.useState(VMTab.General); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <TabsWidget | ||||
|         currTab={currTab} | ||||
|         onTabChange={setCurrTab} | ||||
|         options={[ | ||||
|           { label: "General", value: VMTab.General, visible: true }, | ||||
|           { label: "Storage", value: VMTab.Storage, visible: true }, | ||||
|           { label: "Network", value: VMTab.Network, visible: true }, | ||||
|           { | ||||
|             label: "XML", | ||||
|             value: VMTab.XML, | ||||
|             visible: !p.editable, | ||||
|           }, | ||||
|           { | ||||
|             label: "Danger zone", | ||||
|             value: VMTab.Danger, | ||||
|             visible: !p.editable, | ||||
|             color: "red", | ||||
|           }, | ||||
|         ]} | ||||
|       /> | ||||
|  | ||||
|       {currTab === VMTab.General && <VMDetailsTabGeneral {...p} />} | ||||
|       {currTab === VMTab.Storage && <VMDetailsTabStorage {...p} />} | ||||
|       {currTab === VMTab.Network && <VMDetailsTabNetwork {...p} />} | ||||
|       {currTab === VMTab.XML && <VMDetailsTabXML {...p} />} | ||||
|       {currTab === VMTab.Danger && <VMDetailsTabDanger {...p} />} | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function VMDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement { | ||||
|   return ( | ||||
|     <Grid container spacing={2}> | ||||
|       { | ||||
| @@ -208,25 +264,99 @@ function VMDetailsInner( | ||||
|           /> | ||||
|         )} | ||||
|       </EditSection> | ||||
|  | ||||
|       {/* Storage section */} | ||||
|       <EditSection title="Storage"> | ||||
|         <VMSelectIsoInput | ||||
|           editable={p.editable} | ||||
|           isoList={p.isoList} | ||||
|           attachedISOs={p.vm.iso_files} | ||||
|           onChange={(v) => { | ||||
|             p.vm.iso_files = v; | ||||
|             p.onChange?.(); | ||||
|           }} | ||||
|         /> | ||||
|         <VMDisksList {...p} /> | ||||
|       </EditSection> | ||||
|  | ||||
|       {/* Networks section */} | ||||
|       <EditSection title="Networks"> | ||||
|         <VMNetworksList {...p} /> | ||||
|       </EditSection> | ||||
|     </Grid> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Storage section | ||||
|  */ | ||||
| function VMDetailsTabStorage(p: DetailsInnerProps): React.ReactElement { | ||||
|   return ( | ||||
|     <Grid container spacing={2}> | ||||
|       {(p.editable || p.vm.disks.length > 0) && ( | ||||
|         <EditSection title="Disks storage"> | ||||
|           <VMDisksList {...p} /> | ||||
|         </EditSection> | ||||
|       )} | ||||
|  | ||||
|       {(p.editable || p.vm.iso_files.length > 0) && ( | ||||
|         <EditSection title="ISO storage"> | ||||
|           <VMSelectIsoInput | ||||
|             editable={p.editable} | ||||
|             isoList={p.isoList} | ||||
|             attachedISOs={p.vm.iso_files} | ||||
|             onChange={(v) => { | ||||
|               p.vm.iso_files = v; | ||||
|               p.onChange?.(); | ||||
|             }} | ||||
|           /> | ||||
|         </EditSection> | ||||
|       )} | ||||
|     </Grid> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function VMDetailsTabNetwork(p: DetailsInnerProps): React.ReactElement { | ||||
|   return <VMNetworksList {...p} />; | ||||
| } | ||||
|  | ||||
| function VMDetailsTabXML(p: DetailsInnerProps): React.ReactElement { | ||||
|   return ( | ||||
|     <XMLAsyncWidget | ||||
|       errMsg="Failed to load VM XML source definition!" | ||||
|       identifier={p.vm.uuid!} | ||||
|       load={() => VMApi.GetSingleXML(p.vm.uuid!)} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function VMDetailsTabDanger(p: DetailsInnerProps): React.ReactElement { | ||||
|   const confirm = useConfirm(); | ||||
|   const alert = useAlert(); | ||||
|   const snackbar = useSnackbar(); | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const deleteVM = async () => { | ||||
|     try { | ||||
|       if ( | ||||
|         !(await confirm( | ||||
|           `Do you really want to delete the vm ${p.vm.name}? The operation CANNOT be undone!`, | ||||
|           "Delete a VM", | ||||
|           "DELETE" | ||||
|         )) | ||||
|       ) | ||||
|         return; | ||||
|  | ||||
|       const keepData = !(await confirm( | ||||
|         "Do you want to delete the files of the VM?", | ||||
|         "Delete a VM", | ||||
|         "Delete the data", | ||||
|         "keep the data" | ||||
|       )); | ||||
|  | ||||
|       if ( | ||||
|         !(await confirm( | ||||
|           `[LAST CALL] Do you really want to procede with removal? Again, the operation CANNOT be undone!`, | ||||
|           "Delete a VM", | ||||
|           "DELETE" | ||||
|         )) | ||||
|       ) | ||||
|         return; | ||||
|  | ||||
|       await VMApi.Delete(p.vm, keepData); | ||||
|       snackbar("The VM was successfully deleted!"); | ||||
|  | ||||
|       navigate("/vms"); | ||||
|     } catch (e) { | ||||
|       console.error(e); | ||||
|       alert(`Failed to delete VM!\n${e}`); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <Button color="error" onClick={deleteVM}> | ||||
|       Delete the VM | ||||
|     </Button> | ||||
|   ); | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user