Compare commits
	
		
			59 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 524ab50df7 | |||
| 8cd32d35e2 | |||
| 307e5d1b50 | |||
| ff66a5cf97 | |||
| dcf6cdab9b | |||
| 2649bfbd25 | |||
| 3eab3ba4b5 | |||
| 975b4ab395 | |||
| c40ee037da | |||
| 719ab3b265 | |||
| ad45c0d654 | |||
| 7d7a052f5f | |||
| aafa4bf145 | |||
| baa0adf529 | |||
| fdd005a3ec | |||
| ed48b22f7f | |||
| a7bfb80547 | |||
| 0710c61909 | |||
| 85dcb06014 | |||
| c880c5e6bb | |||
| 22f5acd0ff | |||
| 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) | ||||
							
								
								
									
										30
									
								
								virtweb_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										30
									
								
								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" | ||||
| @@ -2149,18 +2159,6 @@ dependencies = [ | ||||
|  "serde_derive", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "serde-xml-rs" | ||||
| version = "0.6.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782" | ||||
| dependencies = [ | ||||
|  "log", | ||||
|  "serde", | ||||
|  "thiserror", | ||||
|  "xml-rs", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "serde_derive" | ||||
| version = "1.0.193" | ||||
| @@ -2662,11 +2660,11 @@ dependencies = [ | ||||
|  "log", | ||||
|  "mime_guess", | ||||
|  "num", | ||||
|  "quick-xml", | ||||
|  "rand", | ||||
|  "reqwest", | ||||
|  "rust-embed", | ||||
|  "serde", | ||||
|  "serde-xml-rs", | ||||
|  "serde_json", | ||||
|  "sysinfo", | ||||
|  "tempfile", | ||||
| @@ -2970,12 +2968,6 @@ dependencies = [ | ||||
|  "windows-sys 0.48.0", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "xml-rs" | ||||
| version = "0.8.19" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" | ||||
|  | ||||
| [[package]] | ||||
| name = "zerocopy" | ||||
| version = "0.7.31" | ||||
|   | ||||
| @@ -22,7 +22,7 @@ actix-web-actors = "4.2.0" | ||||
| 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 +43,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,34 @@ 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", | ||||
| ]; | ||||
|  | ||||
| /// List of valid network chains | ||||
| pub const NETWORK_CHAINS: [&str; 8] = ["root", "mac", "stp", "vlan", "arp", "rarp", "ipv4", "ipv6"]; | ||||
|   | ||||
| @@ -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}; | ||||
|  | ||||
| @@ -15,6 +15,8 @@ struct StaticConfig { | ||||
|     oidc_auth_enabled: bool, | ||||
|     iso_mimetypes: &'static [&'static str], | ||||
|     net_mac_prefix: &'static str, | ||||
|     builtin_nwfilter_rules: &'static [&'static str], | ||||
|     nwfilter_chains: &'static [&'static str], | ||||
|     constraints: ServerConstraints, | ||||
| } | ||||
|  | ||||
| @@ -24,6 +26,12 @@ struct LenConstraints { | ||||
|     max: usize, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize)] | ||||
| struct SLenConstraints { | ||||
|     min: i64, | ||||
|     max: i64, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize)] | ||||
| struct ServerConstraints { | ||||
|     iso_max_size: usize, | ||||
| @@ -36,6 +44,10 @@ struct ServerConstraints { | ||||
|     net_name_size: LenConstraints, | ||||
|     net_title_size: LenConstraints, | ||||
|     dhcp_reservation_host_name: LenConstraints, | ||||
|     nwfilter_name_size: LenConstraints, | ||||
|     nwfilter_comment_size: LenConstraints, | ||||
|     nwfilter_priority: SLenConstraints, | ||||
|     nwfilter_selectors_count: LenConstraints, | ||||
| } | ||||
|  | ||||
| pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder { | ||||
| @@ -45,6 +57,8 @@ 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_nwfilter_rules: &constants::BUILTIN_NETWORK_FILTER_RULES, | ||||
|         nwfilter_chains: &constants::NETWORK_CHAINS, | ||||
|         constraints: ServerConstraints { | ||||
|             iso_max_size: constants::ISO_MAX_SIZE, | ||||
|  | ||||
| @@ -69,6 +83,14 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder { | ||||
|             net_title_size: LenConstraints { min: 0, max: 50 }, | ||||
|  | ||||
|             dhcp_reservation_host_name: LenConstraints { min: 2, max: 250 }, | ||||
|  | ||||
|             nwfilter_name_size: LenConstraints { min: 2, max: 250 }, | ||||
|             nwfilter_comment_size: LenConstraints { min: 0, max: 256 }, | ||||
|             nwfilter_priority: SLenConstraints { | ||||
|                 min: -1000, | ||||
|                 max: 1000, | ||||
|             }, | ||||
|             nwfilter_selectors_count: LenConstraints { min: 0, max: 1 }, | ||||
|         }, | ||||
|     }) | ||||
| } | ||||
|   | ||||
| @@ -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)?) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										240
									
								
								virtweb_backend/src/libvirt_lib_structures/nwfilter.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										240
									
								
								virtweb_backend/src/libvirt_lib_structures/nwfilter.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,240 @@ | ||||
| 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 = "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)] | ||||
| #[serde(rename = "all")] | ||||
| pub struct NetworkFilterRuleProtocolAllXML<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 = "@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 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 all protocols | ||||
|     #[serde(default, rename = "all", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub all_selectors: Vec<NetworkFilterRuleProtocolAllXML<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>>, | ||||
|  | ||||
|     /// Match all ipv6 protocols | ||||
|     #[serde(default, rename = "all-ipv6", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub all_ipv6_selectors: Vec<NetworkFilterRuleProtocolAllXML<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(), | ||||
|                     }), | ||||
|                 }), | ||||
|         }) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										925
									
								
								virtweb_backend/src/libvirt_rest_structures/nw_filter.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										925
									
								
								virtweb_backend/src/libvirt_rest_structures/nw_filter.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,925 @@ | ||||
| use crate::libvirt_lib_structures::nwfilter::{ | ||||
|     NetworkFilterRefXML, NetworkFilterRuleProtocolAllXML, 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)] | ||||
| pub struct NetworkSelectorAll<IPv> { | ||||
|     comment: Option<String>, | ||||
|     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>, | ||||
|     state: Option<Layer4State>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| #[serde(tag = "type", rename_all = "lowercase")] | ||||
| pub enum NetworkFilterSelector { | ||||
|     Mac(NetworkSelectorMac), | ||||
|     Arp(NetworkSelectorARP), | ||||
|     Rarp(NetworkSelectorARP), | ||||
|     IPv4(NetworkFilterSelectorIP<4>), | ||||
|     IPv6(NetworkFilterSelectorIP<6>), | ||||
|     TCP(NetworkFilterSelectorLayer4<Ipv4Addr>), | ||||
|     UDP(NetworkFilterSelectorLayer4<Ipv4Addr>), | ||||
|     SCTP(NetworkFilterSelectorLayer4<Ipv4Addr>), | ||||
|     ICMP(NetworkFilterSelectorLayer4<Ipv4Addr>), | ||||
|     All(NetworkSelectorAll<Ipv4Addr>), | ||||
|     TCPipv6(NetworkFilterSelectorLayer4<Ipv6Addr>), | ||||
|     UDPipv6(NetworkFilterSelectorLayer4<Ipv6Addr>), | ||||
|     SCTPipv6(NetworkFilterSelectorLayer4<Ipv6Addr>), | ||||
|     ICMPipv6(NetworkFilterSelectorLayer4<Ipv6Addr>), | ||||
|     Allipv6(NetworkSelectorAll<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_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(), | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn lib2rest_process_all_rule<IPv: Copy>( | ||||
|         n: &NetworkFilterRuleProtocolAllXML<IPv>, | ||||
|     ) -> anyhow::Result<NetworkSelectorAll<IPv>> { | ||||
|         Ok(NetworkSelectorAll { | ||||
|             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, | ||||
|             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(); | ||||
|  | ||||
|             // Mac selectors | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .mac_selectors | ||||
|                     .iter() | ||||
|                     .map(Self::lib2rest_process_mac_rule) | ||||
|                     .collect(), | ||||
|             ); | ||||
|  | ||||
|             // ARP - RARP selectors | ||||
|             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 selectors | ||||
|             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 | ||||
|             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>>()?, | ||||
|             ); | ||||
|  | ||||
|             // All selectors | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .all_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| { | ||||
|                         Ok(NetworkFilterSelector::All(Self::lib2rest_process_all_rule( | ||||
|                             r, | ||||
|                         )?)) | ||||
|                     }) | ||||
|                     .collect::<Result<Vec<_>, anyhow::Error>>()?, | ||||
|             ); | ||||
|  | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .all_ipv6_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| { | ||||
|                         Ok(NetworkFilterSelector::Allipv6( | ||||
|                             Self::lib2rest_process_all_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_all_selector<IPv: Copy>( | ||||
|         selector: &NetworkSelectorAll<IPv>, | ||||
|     ) -> anyhow::Result<NetworkFilterRuleProtocolAllXML<IPv>> { | ||||
|         Ok(NetworkFilterRuleProtocolAllXML { | ||||
|             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, | ||||
|             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::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::All(all) => { | ||||
|                     rule_xml | ||||
|                         .all_selectors | ||||
|                         .push(Self::rest2lib_process_all_selector(all)?); | ||||
|                 } | ||||
|  | ||||
|                 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)?), | ||||
|  | ||||
|                 NetworkFilterSelector::Allipv6(all) => { | ||||
|                     rule_xml | ||||
|                         .all_ipv6_selectors | ||||
|                         .push(Self::rest2lib_process_all_selector(all)?); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         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,9 +25,13 @@ 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"; | ||||
| import { NetworkFiltersListRoute } from "./routes/NetworkFiltersListRoute"; | ||||
| import { ViewNWFilterRoute } from "./routes/ViewNWFilterRoute"; | ||||
| import { | ||||
|   CreateNWFilterRoute, | ||||
|   EditNWFilterRoute, | ||||
| } from "./routes/EditNWFilterRoute"; | ||||
|  | ||||
| interface AuthContext { | ||||
|   signedIn: boolean; | ||||
| @@ -57,13 +61,16 @@ 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="nwfilter" element={<NetworkFiltersListRoute />} /> | ||||
|           <Route path="nwfilter/new" element={<CreateNWFilterRoute />} /> | ||||
|           <Route path="nwfilter/:uuid" element={<ViewNWFilterRoute />} /> | ||||
|           <Route path="nwfilter/:uuid/edit" element={<EditNWFilterRoute />} /> | ||||
|  | ||||
|           <Route path="sysinfo" element={<SysInfoRoute />} /> | ||||
|           <Route path="*" element={<NotFoundRoute />} /> | ||||
|   | ||||
							
								
								
									
										227
									
								
								virtweb_frontend/src/api/NWFilterApi.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										227
									
								
								virtweb_frontend/src/api/NWFilterApi.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,227 @@ | ||||
| import { APIClient } from "./ApiClient"; | ||||
| import { ServerApi } from "./ServerApi"; | ||||
|  | ||||
| export interface NWFilterChain { | ||||
|   protocol: string; | ||||
|   suffix?: string; | ||||
| } | ||||
|  | ||||
| export interface NWFSMac { | ||||
|   type: "mac"; | ||||
|   src_mac_addr?: string; | ||||
|   src_mac_mask?: string; | ||||
|   dst_mac_addr?: string; | ||||
|   dst_mac_mask?: string; | ||||
|   comment?: string; | ||||
| } | ||||
|  | ||||
| export interface NWFSArpOrRARP { | ||||
|   srcmacaddr?: string; | ||||
|   srcmacmask?: string; | ||||
|   dstmacaddr?: string; | ||||
|   dstmacmask?: string; | ||||
|   arpsrcipaddr?: string; | ||||
|   arpsrcipmask?: number; | ||||
|   arpdstipaddr?: string; | ||||
|   arpdstipmask?: number; | ||||
|   comment?: string; | ||||
| } | ||||
|  | ||||
| export type NWFSArp = NWFSArpOrRARP & { | ||||
|   type: "arp"; | ||||
| }; | ||||
|  | ||||
| export type NWFSRArp = NWFSArpOrRARP & { | ||||
|   type: "rarp"; | ||||
| }; | ||||
|  | ||||
| export interface NWFSIPBase { | ||||
|   srcmacaddr?: string; | ||||
|   srcmacmask?: string; | ||||
|   dstmacaddr?: string; | ||||
|   dstmacmask?: string; | ||||
|   srcipaddr?: string; | ||||
|   srcipmask?: number; | ||||
|   dstipaddr?: string; | ||||
|   dstipmask?: number; | ||||
|   comment?: string; | ||||
| } | ||||
|  | ||||
| export type NFWSIPv4 = NWFSIPBase & { type: "ipv4" }; | ||||
| export type NFWSIPv6 = NWFSIPBase & { type: "ipv6" }; | ||||
|  | ||||
| export type Layer4State = | ||||
|   | "NEW" | ||||
|   | "ESTABLISHED" | ||||
|   | "RELATED" | ||||
|   | "INVALID" | ||||
|   | "NONE"; | ||||
|  | ||||
| export interface NWFSLayer4Base { | ||||
|   srcmacaddr?: string; | ||||
|   srcipaddr?: string; | ||||
|   srcipmask?: number; | ||||
|   dstipaddr?: string; | ||||
|   dstipmask?: number; | ||||
|   srcipfrom?: string; | ||||
|   srcipto?: string; | ||||
|   dstipfrom?: string; | ||||
|   dstipto?: string; | ||||
|   srcportstart?: number; | ||||
|   srcportend?: number; | ||||
|   dstportstart?: number; | ||||
|   dstportend?: number; | ||||
|   state?: Layer4State; | ||||
|   comment?: string; | ||||
| } | ||||
|  | ||||
| export type NFWSTCPv4 = NWFSLayer4Base & { type: "tcp" }; | ||||
| export type NFWSUDPv4 = NWFSLayer4Base & { type: "udp" }; | ||||
| export type NFWSSCTPv4 = NWFSLayer4Base & { type: "sctp" }; | ||||
| export type NFWSICMPv4 = NWFSLayer4Base & { type: "icmp" }; | ||||
|  | ||||
| export type NFWSTCPv6 = NWFSLayer4Base & { type: "tcpipv6" }; | ||||
| export type NFWSUDPv6 = NWFSLayer4Base & { type: "udpipv6" }; | ||||
| export type NFWSSCTPv6 = NWFSLayer4Base & { type: "sctpipv6" }; | ||||
| export type NFWSICMPv6 = NWFSLayer4Base & { type: "icmpipv6" }; | ||||
|  | ||||
| export interface NWFSAllBase { | ||||
|   srcmacaddr?: string; | ||||
|   srcipaddr?: string; | ||||
|   srcipmask?: number; | ||||
|   dstipaddr?: string; | ||||
|   dstipmask?: number; | ||||
|   srcipfrom?: string; | ||||
|   srcipto?: string; | ||||
|   dstipfrom?: string; | ||||
|   dstipto?: string; | ||||
|   state?: Layer4State; | ||||
|   comment?: string; | ||||
| } | ||||
|  | ||||
| export type NWFSAll = NWFSAllBase & { | ||||
|   type: "all"; | ||||
| }; | ||||
|  | ||||
| export type NWFSAllIPv6 = NWFSAllBase & { | ||||
|   type: "allipv6"; | ||||
| }; | ||||
|  | ||||
| export type NWFSelector = | ||||
|   | NWFSMac | ||||
|   | NWFSArp | ||||
|   | NWFSRArp | ||||
|   | NFWSIPv4 | ||||
|   | NFWSIPv6 | ||||
|   | NFWSTCPv4 | ||||
|   | NFWSUDPv4 | ||||
|   | NFWSSCTPv4 | ||||
|   | NFWSICMPv4 | ||||
|   | NWFSAll | ||||
|   | NFWSTCPv6 | ||||
|   | NFWSUDPv6 | ||||
|   | NFWSSCTPv6 | ||||
|   | NFWSICMPv6 | ||||
|   | NWFSAllIPv6; | ||||
|  | ||||
| export interface NWFilterRule { | ||||
|   action: "drop" | "reject" | "accept" | "return" | "continue"; | ||||
|   direction: "in" | "out" | "inout"; | ||||
|   priority?: number; | ||||
|   selectors: NWFSelector[]; | ||||
| } | ||||
|  | ||||
| export interface NWFilter { | ||||
|   name: string; | ||||
|   uuid?: string; | ||||
|   chain?: NWFilterChain; | ||||
|   priority?: number; | ||||
|   join_filters: string[]; | ||||
|   rules: NWFilterRule[]; | ||||
| } | ||||
|  | ||||
| export function NWFilterURL(n: NWFilter, edit: boolean = false): string { | ||||
|   return `/nwfilter/${n.uuid}${edit ? "/edit" : ""}`; | ||||
| } | ||||
|  | ||||
| export function NWFilterIsBuiltin(n: NWFilter): boolean { | ||||
|   return ServerApi.Config.builtin_nwfilter_rules.includes(n.name); | ||||
| } | ||||
|  | ||||
| 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; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get the information about a single network filter | ||||
|    */ | ||||
|   static async GetSingle(uuid: string): Promise<NWFilter> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         method: "GET", | ||||
|         uri: `/nwfilter/${uuid}`, | ||||
|       }) | ||||
|     ).data; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get the source XML configuration of a network filter for debugging purposes | ||||
|    */ | ||||
|   static async GetSingleXML(uuid: string): Promise<string> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         uri: `/nwfilter/${uuid}/src`, | ||||
|         method: "GET", | ||||
|       }) | ||||
|     ).data; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Create a new network filter | ||||
|    */ | ||||
|   static async Create(n: NWFilter): Promise<{ uid: string }> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         method: "POST", | ||||
|         uri: "/nwfilter/create", | ||||
|         jsonData: n, | ||||
|       }) | ||||
|     ).data; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Update an existing network filter | ||||
|    */ | ||||
|   static async Update(n: NWFilter): Promise<{ uid: string }> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         method: "PUT", | ||||
|         uri: `/nwfilter/${n.uuid}`, | ||||
|         jsonData: n, | ||||
|       }) | ||||
|     ).data; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Delete a network filter | ||||
|    */ | ||||
|   static async Delete(n: NWFilter): Promise<void> { | ||||
|     await APIClient.exec({ | ||||
|       method: "DELETE", | ||||
|       uri: `/nwfilter/${n.uuid}`, | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| @@ -39,10 +39,6 @@ export function NetworkURL(n: NetworkInfo, edit: boolean = false): string { | ||||
|   return `/net/${n.uuid}${edit ? "/edit" : ""}`; | ||||
| } | ||||
|  | ||||
| export function NetworkXMLURL(n: NetworkInfo): string { | ||||
|   return `/net/${n.uuid}/xml`; | ||||
| } | ||||
|  | ||||
| export class NetworkApi { | ||||
|   /** | ||||
|    * Create a new network | ||||
| @@ -164,12 +160,10 @@ export class NetworkApi { | ||||
|   /** | ||||
|    * Delete a network | ||||
|    */ | ||||
|   static async Delete(n: NetworkInfo): Promise<NetworkInfo[]> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         method: "DELETE", | ||||
|         uri: `/network/${n.uuid}`, | ||||
|       }) | ||||
|     ).data; | ||||
|   static async Delete(n: NetworkInfo): Promise<void> { | ||||
|     await APIClient.exec({ | ||||
|       method: "DELETE", | ||||
|       uri: `/network/${n.uuid}`, | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -6,6 +6,8 @@ export interface ServerConfig { | ||||
|   oidc_auth_enabled: boolean; | ||||
|   iso_mimetypes: string[]; | ||||
|   net_mac_prefix: string; | ||||
|   builtin_nwfilter_rules: string[]; | ||||
|   nwfilter_chains: string[]; | ||||
|   constraints: ServerConstraints; | ||||
| } | ||||
|  | ||||
| @@ -20,6 +22,10 @@ export interface ServerConstraints { | ||||
|   net_name_size: LenConstraint; | ||||
|   net_title_size: LenConstraint; | ||||
|   dhcp_reservation_host_name: LenConstraint; | ||||
|   nwfilter_name_size: LenConstraint; | ||||
|   nwfilter_comment_size: LenConstraint; | ||||
|   nwfilter_priority: LenConstraint; | ||||
|   nwfilter_selectors_count: LenConstraint; | ||||
| } | ||||
|  | ||||
| export interface LenConstraint { | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
|  | ||||
| @@ -119,10 +133,6 @@ export class VMInfo implements VMInfoInterface { | ||||
|   get VNCURL(): string { | ||||
|     return `/vm/${this.uuid}/vnc`; | ||||
|   } | ||||
|  | ||||
|   get XMLURL(): string { | ||||
|     return `/vm/${this.uuid}/xml`; | ||||
|   } | ||||
| } | ||||
|  | ||||
| export class VMApi { | ||||
|   | ||||
							
								
								
									
										151
									
								
								virtweb_frontend/src/routes/EditNWFilterRoute.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								virtweb_frontend/src/routes/EditNWFilterRoute.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,151 @@ | ||||
| import { Button } from "@mui/material"; | ||||
| import React from "react"; | ||||
| import { useNavigate, useParams } from "react-router-dom"; | ||||
| import { useAlert } from "../hooks/providers/AlertDialogProvider"; | ||||
| import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider"; | ||||
| import { useSnackbar } from "../hooks/providers/SnackbarProvider"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { ConfigImportExportButtons } from "../widgets/ConfigImportExportButtons"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
| import { NWFilter, NWFilterApi, NWFilterURL } from "../api/NWFilterApi"; | ||||
| import { NWFilterDetails } from "../widgets/nwfilter/NWFilterDetails"; | ||||
|  | ||||
| export function CreateNWFilterRoute(): React.ReactElement { | ||||
|   const alert = useAlert(); | ||||
|   const snackbar = useSnackbar(); | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const [nwfilter, setNWFilter] = React.useState<NWFilter>({ | ||||
|     name: "my-filter", | ||||
|     chain: { protocol: "root" }, | ||||
|     join_filters: [], | ||||
|     rules: [], | ||||
|   }); | ||||
|  | ||||
|   const createNWFilter = async (n: NWFilter) => { | ||||
|     try { | ||||
|       const res = await NWFilterApi.Create(n); | ||||
|       snackbar("The network filter was successfully created!"); | ||||
|       navigate(`/nwfilter/${res.uid}`); | ||||
|     } catch (e) { | ||||
|       console.error(e); | ||||
|       alert(`Failed to create network filter!\n${e}`); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <EditNetworkFilterRouteInner | ||||
|       nwfilter={nwfilter} | ||||
|       creating={true} | ||||
|       onCancel={() => navigate("/nwfilter")} | ||||
|       onSave={createNWFilter} | ||||
|       onReplace={setNWFilter} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export function EditNWFilterRoute(): React.ReactElement { | ||||
|   const alert = useAlert(); | ||||
|   const snackbar = useSnackbar(); | ||||
|  | ||||
|   const { uuid } = useParams(); | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const [nwfilter, setNWFilter] = React.useState<NWFilter | undefined>(); | ||||
|  | ||||
|   const load = async () => { | ||||
|     setNWFilter(await NWFilterApi.GetSingle(uuid!)); | ||||
|   }; | ||||
|  | ||||
|   const updateNetworkFilter = async (n: NWFilter) => { | ||||
|     try { | ||||
|       await NWFilterApi.Update(n); | ||||
|       snackbar("The network filter was successfully updated!"); | ||||
|       navigate(NWFilterURL(nwfilter!)); | ||||
|     } catch (e) { | ||||
|       console.error(e); | ||||
|       alert(`Failed to update network filter!\n${e}`); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <AsyncWidget | ||||
|       loadKey={uuid} | ||||
|       ready={nwfilter !== undefined} | ||||
|       errMsg="Failed to fetch network filter information!" | ||||
|       load={load} | ||||
|       build={() => ( | ||||
|         <EditNetworkFilterRouteInner | ||||
|           nwfilter={nwfilter!} | ||||
|           creating={false} | ||||
|           onCancel={() => navigate(`/nwfilter/${uuid}`)} | ||||
|           onSave={updateNetworkFilter} | ||||
|           onReplace={setNWFilter} | ||||
|         /> | ||||
|       )} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function EditNetworkFilterRouteInner(p: { | ||||
|   nwfilter: NWFilter; | ||||
|   creating: boolean; | ||||
|   onCancel: () => void; | ||||
|   onSave: (vm: NWFilter) => Promise<void>; | ||||
|   onReplace: (vm: NWFilter) => void; | ||||
| }): React.ReactElement { | ||||
|   const loadingMessage = useLoadingMessage(); | ||||
|  | ||||
|   const [changed, setChanged] = React.useState(false); | ||||
|  | ||||
|   const [, updateState] = React.useState<any>(); | ||||
|   const forceUpdate = React.useCallback(() => updateState({}), []); | ||||
|  | ||||
|   const valueChanged = () => { | ||||
|     setChanged(true); | ||||
|     forceUpdate(); | ||||
|   }; | ||||
|  | ||||
|   const save = async () => { | ||||
|     loadingMessage.show("Saving network filter configuration..."); | ||||
|     await p.onSave(p.nwfilter); | ||||
|     loadingMessage.hide(); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <VirtWebRouteContainer | ||||
|       label={p.creating ? "Create a Network Filter" : "Edit Network Filter"} | ||||
|       actions={ | ||||
|         <span> | ||||
|           <ConfigImportExportButtons | ||||
|             currentConf={p.nwfilter} | ||||
|             filename={`nwfilter-${p.nwfilter.name}.json`} | ||||
|             importConf={(c) => { | ||||
|               p.onReplace(c); | ||||
|               valueChanged(); | ||||
|             }} | ||||
|           /> | ||||
|  | ||||
|           {changed && ( | ||||
|             <Button | ||||
|               variant="contained" | ||||
|               onClick={save} | ||||
|               style={{ marginRight: "10px" }} | ||||
|             > | ||||
|               {p.creating ? "Create" : "Save"} | ||||
|             </Button> | ||||
|           )} | ||||
|           <Button onClick={p.onCancel} variant="outlined"> | ||||
|             Cancel | ||||
|           </Button> | ||||
|         </span> | ||||
|       } | ||||
|     > | ||||
|       <NWFilterDetails | ||||
|         nwfilter={p.nwfilter} | ||||
|         editable={true} | ||||
|         onChange={valueChanged} | ||||
|       /> | ||||
|     </VirtWebRouteContainer> | ||||
|   ); | ||||
| } | ||||
| @@ -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> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										154
									
								
								virtweb_frontend/src/routes/NetworkFiltersListRoute.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								virtweb_frontend/src/routes/NetworkFiltersListRoute.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,154 @@ | ||||
| import VisibilityIcon from "@mui/icons-material/Visibility"; | ||||
| import { | ||||
|   Button, | ||||
|   IconButton, | ||||
|   Paper, | ||||
|   Table, | ||||
|   TableBody, | ||||
|   TableCell, | ||||
|   TableContainer, | ||||
|   TableHead, | ||||
|   TableRow, | ||||
|   ToggleButton, | ||||
|   ToggleButtonGroup, | ||||
|   Typography, | ||||
| } from "@mui/material"; | ||||
| import React from "react"; | ||||
| import { useNavigate } from "react-router-dom"; | ||||
| import { | ||||
|   NWFilter, | ||||
|   NWFilterApi, | ||||
|   NWFilterIsBuiltin, | ||||
|   NWFilterURL, | ||||
| } from "../api/NWFilterApi"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { RouterLink } from "../widgets/RouterLink"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
|  | ||||
| export function NetworkFiltersListRoute(): React.ReactElement { | ||||
|   const [list, setList] = React.useState<NWFilter[] | undefined>(); | ||||
|  | ||||
|   const [count] = React.useState(1); | ||||
|  | ||||
|   const load = async () => { | ||||
|     setList(await NWFilterApi.GetList()); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <AsyncWidget | ||||
|       loadKey={count} | ||||
|       load={load} | ||||
|       ready={list !== undefined} | ||||
|       errMsg="Failed to load the list of networks!" | ||||
|       build={() => <NetworkFiltersListRouteInner list={list!} />} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| enum VisibleFilters { | ||||
|   All, | ||||
|   Builtin, | ||||
|   Custom, | ||||
| } | ||||
|  | ||||
| function NetworkFiltersListRouteInner(p: { | ||||
|   list: NWFilter[]; | ||||
| }): React.ReactElement { | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const [visibleFilters, setVisibleFilters] = React.useState( | ||||
|     VisibleFilters.All | ||||
|   ); | ||||
|  | ||||
|   const filteredList = React.useMemo(() => { | ||||
|     if (visibleFilters === VisibleFilters.All) return p.list; | ||||
|  | ||||
|     const onlyBuiltin = visibleFilters === VisibleFilters.Builtin; | ||||
|  | ||||
|     return p.list.filter((f) => NWFilterIsBuiltin(f) === onlyBuiltin); | ||||
|   }, [visibleFilters]); | ||||
|  | ||||
|   return ( | ||||
|     <VirtWebRouteContainer | ||||
|       label="Network filters" | ||||
|       actions={ | ||||
|         <> | ||||
|           <span style={{ flex: 10 }}></span> | ||||
|           <ToggleButtonGroup | ||||
|             size="small" | ||||
|             value={visibleFilters} | ||||
|             exclusive | ||||
|             onChange={(_ev, v) => setVisibleFilters(v)} | ||||
|             aria-label="visible filters" | ||||
|           > | ||||
|             <ToggleButton value={VisibleFilters.All}>All</ToggleButton> | ||||
|             <ToggleButton value={VisibleFilters.Builtin}>Builtin</ToggleButton> | ||||
|             <ToggleButton value={VisibleFilters.Custom}>Custom</ToggleButton> | ||||
|           </ToggleButtonGroup> | ||||
|           <span style={{ flex: 2 }}></span> | ||||
|  | ||||
|           <RouterLink to="/nwfilter/new"> | ||||
|             <Button>New</Button> | ||||
|           </RouterLink> | ||||
|         </> | ||||
|       } | ||||
|     > | ||||
|       <TableContainer component={Paper}> | ||||
|         <Table> | ||||
|           <TableHead> | ||||
|             <TableRow> | ||||
|               <TableCell>Name</TableCell> | ||||
|               <TableCell>Chain</TableCell> | ||||
|               <TableCell>Priority</TableCell> | ||||
|               <TableCell>Referenced filters</TableCell> | ||||
|               <TableCell># of rules</TableCell> | ||||
|               <TableCell>Actions</TableCell> | ||||
|             </TableRow> | ||||
|           </TableHead> | ||||
|           <TableBody> | ||||
|             {filteredList.map((t) => { | ||||
|               return ( | ||||
|                 <TableRow | ||||
|                   key={t.uuid} | ||||
|                   hover | ||||
|                   onDoubleClick={() => navigate(NWFilterURL(t))} | ||||
|                 > | ||||
|                   <TableCell>{t.name}</TableCell> | ||||
|                   <TableCell> | ||||
|                     {t.chain?.protocol ?? ( | ||||
|                       <Typography style={{ fontStyle: "italic" }}> | ||||
|                         None | ||||
|                       </Typography> | ||||
|                     )} | ||||
|                   </TableCell> | ||||
|                   <TableCell> | ||||
|                     {t.priority ?? ( | ||||
|                       <Typography style={{ fontStyle: "italic" }}> | ||||
|                         None | ||||
|                       </Typography> | ||||
|                     )} | ||||
|                   </TableCell> | ||||
|                   <TableCell> | ||||
|                     <ul> | ||||
|                       {t.join_filters.map((f, n) => ( | ||||
|                         <li key={n}>{f}</li> | ||||
|                       ))} | ||||
|                     </ul> | ||||
|                   </TableCell> | ||||
|                   <TableCell>{t.rules.length}</TableCell> | ||||
|                   <TableCell> | ||||
|                     <RouterLink to={NWFilterURL(t)}> | ||||
|                       <IconButton> | ||||
|                         <VisibilityIcon /> | ||||
|                       </IconButton> | ||||
|                     </RouterLink> | ||||
|                   </TableCell> | ||||
|                 </TableRow> | ||||
|               ); | ||||
|             })} | ||||
|           </TableBody> | ||||
|         </Table> | ||||
|       </TableContainer> | ||||
|     </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> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										65
									
								
								virtweb_frontend/src/routes/ViewNWFilterRoute.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								virtweb_frontend/src/routes/ViewNWFilterRoute.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| import { Button } from "@mui/material"; | ||||
| import React from "react"; | ||||
| import { useNavigate, useParams } from "react-router-dom"; | ||||
| import { | ||||
|   NWFilter, | ||||
|   NWFilterApi, | ||||
|   NWFilterIsBuiltin, | ||||
|   NWFilterURL, | ||||
| } from "../api/NWFilterApi"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { ConfigImportExportButtons } from "../widgets/ConfigImportExportButtons"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
| import { NWFilterDetails } from "../widgets/nwfilter/NWFilterDetails"; | ||||
|  | ||||
| export function ViewNWFilterRoute() { | ||||
|   const { uuid } = useParams(); | ||||
|  | ||||
|   const [nwfilter, setNWFilter] = React.useState<NWFilter | undefined>(); | ||||
|  | ||||
|   const load = async () => { | ||||
|     setNWFilter(await NWFilterApi.GetSingle(uuid!)); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <AsyncWidget | ||||
|       loadKey={uuid} | ||||
|       ready={nwfilter !== undefined} | ||||
|       errMsg="Failed to fetch network filter information!" | ||||
|       load={load} | ||||
|       build={() => <ViewNetworkFilterRouteInner nwfilter={nwfilter!} />} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function ViewNetworkFilterRouteInner(p: { | ||||
|   nwfilter: NWFilter; | ||||
| }): React.ReactElement { | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   return ( | ||||
|     <VirtWebRouteContainer | ||||
|       label={`Network filter ${p.nwfilter.name}`} | ||||
|       actions={ | ||||
|         <span style={{ display: "flex", alignItems: "center" }}> | ||||
|           <ConfigImportExportButtons | ||||
|             filename={`nwfilter-${p.nwfilter.name}.json`} | ||||
|             currentConf={p.nwfilter} | ||||
|           /> | ||||
|  | ||||
|           {!NWFilterIsBuiltin(p.nwfilter) && ( | ||||
|             <Button | ||||
|               variant="contained" | ||||
|               style={{ marginLeft: "15px" }} | ||||
|               onClick={() => navigate(NWFilterURL(p.nwfilter, true))} | ||||
|             > | ||||
|               Edit | ||||
|             </Button> | ||||
|           )} | ||||
|         </span> | ||||
|       } | ||||
|     > | ||||
|       <NWFilterDetails nwfilter={p.nwfilter} editable={false} /> | ||||
|     </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 | ||||
|   | ||||
							
								
								
									
										5
									
								
								virtweb_frontend/src/utils/DebugUtils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								virtweb_frontend/src/utils/DebugUtils.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| export function isDebug(): boolean { | ||||
|   return ( | ||||
|     !import.meta.env.NODE_ENV || import.meta.env.NODE_ENV === "development" | ||||
|   ); | ||||
| } | ||||
| @@ -3,7 +3,8 @@ import { | ||||
|   mdiDisc, | ||||
|   mdiHome, | ||||
|   mdiInformation, | ||||
|   mdiLan | ||||
|   mdiLan, | ||||
|   mdiSecurityNetwork, | ||||
| } from "@mdi/js"; | ||||
| import Icon from "@mdi/react"; | ||||
| import { | ||||
| @@ -15,6 +16,7 @@ import { | ||||
|   ListItemText, | ||||
| } from "@mui/material"; | ||||
| import { Outlet, useLocation } from "react-router-dom"; | ||||
| import { isDebug } from "../utils/DebugUtils"; | ||||
| import { RouterLink } from "./RouterLink"; | ||||
| import { VirtWebAppBar } from "./VirtWebAppBar"; | ||||
|  | ||||
| @@ -60,6 +62,11 @@ export function BaseAuthenticatedPage(): React.ReactElement { | ||||
|             uri="/net" | ||||
|             icon={<Icon path={mdiLan} size={1} />} | ||||
|           /> | ||||
|           <NavLink | ||||
|             label="Network filters" | ||||
|             uri="/nwfilter" | ||||
|             icon={<Icon path={mdiSecurityNetwork} size={1} />} | ||||
|           /> | ||||
|           <NavLink | ||||
|             label="ISO files" | ||||
|             uri="/iso" | ||||
|   | ||||
							
								
								
									
										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> | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import React from "react"; | ||||
| import { TextInput } from "./TextInput"; | ||||
|  | ||||
| export function IPInput(p: { | ||||
| @@ -18,6 +19,47 @@ export function IPInput(p: { | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export function IPInputWithMask(p: { | ||||
|   label: string; | ||||
|   editable: boolean; | ||||
|   ip?: string; | ||||
|   mask?: number; | ||||
|   onValueChange?: (ip?: string, mask?: number) => void; | ||||
|   version: 4 | 6; | ||||
| }): React.ReactElement { | ||||
|   const showSlash = React.useRef(!!p.mask); | ||||
|  | ||||
|   const currValue = | ||||
|     (p.ip ?? "") + (p.mask || showSlash.current ? "/" : "") + (p.mask ?? ""); | ||||
|  | ||||
|   const { onValueChange, ...props } = p; | ||||
|   return ( | ||||
|     <TextInput | ||||
|       onValueChange={(v) => { | ||||
|         showSlash.current = false; | ||||
|         if (!v) { | ||||
|           onValueChange?.(undefined, undefined); | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         const split = v?.split("/"); | ||||
|         const ip = | ||||
|           p.version === 4 ? sanitizeIpV4(split[0]) : sanitizeIpV6(split[0]); | ||||
|         let mask = undefined; | ||||
|  | ||||
|         if (split.length > 1) { | ||||
|           showSlash.current = true; | ||||
|           mask = sanitizeMask(p.version, split[1]); | ||||
|         } | ||||
|  | ||||
|         onValueChange?.(ip, mask); | ||||
|       }} | ||||
|       value={currValue} | ||||
|       {...props} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function sanitizeIpV4(s: string | undefined): string | undefined { | ||||
|   if (s === "" || s === undefined) return s; | ||||
|  | ||||
| @@ -77,3 +119,15 @@ function sanitizeIpV6(s: string | undefined): string | undefined { | ||||
|  | ||||
|   return needAnotherIteration ? sanitizeIpV6(res) : res; | ||||
| } | ||||
|  | ||||
| function sanitizeMask(version: 4 | 6, mask?: string): number | undefined { | ||||
|   if (!mask) return undefined; | ||||
|  | ||||
|   const value = Math.floor(Number(mask)); | ||||
|  | ||||
|   if (version === 4) { | ||||
|     return value < 0 || value > 32 ? 32 : value; | ||||
|   } else { | ||||
|     return value < 0 || value > 64 ? 64 : value; | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										27
									
								
								virtweb_frontend/src/widgets/forms/NWFConnStateInput.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								virtweb_frontend/src/widgets/forms/NWFConnStateInput.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| import { Layer4State } from "../../api/NWFilterApi"; | ||||
| import { SelectInput } from "./SelectInput"; | ||||
|  | ||||
| export function NWFConnStateInput(p: { | ||||
|   editable: boolean; | ||||
|   value?: Layer4State; | ||||
|   onChange: (s?: Layer4State) => void; | ||||
| }): React.ReactElement { | ||||
|   return ( | ||||
|     <SelectInput | ||||
|       {...p} | ||||
|       label="Connection state" | ||||
|       value={p.value} | ||||
|       onValueChange={(s) => { | ||||
|         p.onChange?.(s as any); | ||||
|       }} | ||||
|       options={[ | ||||
|         { label: "None", value: undefined }, | ||||
|         { label: "NEW", value: "NEW" }, | ||||
|         { label: "ESTABLISHED", value: "ESTABLISHED" }, | ||||
|         { label: "RELATED", value: "RELATED" }, | ||||
|         { label: "INVALID", value: "INVALID" }, | ||||
|         { label: "NONE", value: "NONE" }, | ||||
|       ]} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| @@ -0,0 +1,66 @@ | ||||
| import React from "react"; | ||||
| import { useNavigate } from "react-router-dom"; | ||||
| import { NWFilter, NWFilterURL } from "../../api/NWFilterApi"; | ||||
| import { NWFilterItem } from "../nwfilter/NWFilterItem"; | ||||
| import { NWFilterSelectInput } from "./NWFilterSelectInput"; | ||||
|  | ||||
| export function NWFSelectReferencedFilters(p: { | ||||
|   editable: boolean; | ||||
|   selected: string[]; | ||||
|   nwFiltersList: NWFilter[]; | ||||
|   onChange?: () => void; | ||||
|   excludedFilters?: string[]; | ||||
| }): React.ReactElement { | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const nwfilters = React.useMemo( | ||||
|     () => | ||||
|       p.excludedFilters | ||||
|         ? p.nwFiltersList.filter((f) => !p.excludedFilters!.includes(f.name)) | ||||
|         : p.nwFiltersList, | ||||
|     [p.excludedFilters] | ||||
|   ); | ||||
|  | ||||
|   const selectedFilters = React.useMemo( | ||||
|     () => p.selected.map((f) => p.nwFiltersList.find((s) => s.name === f)), | ||||
|     [p.selected.length] | ||||
|   ); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       {selectedFilters.map((entry, n) => ( | ||||
|         <NWFilterItem | ||||
|           key={n} | ||||
|           value={entry} | ||||
|           onDelete={ | ||||
|             p.editable | ||||
|               ? () => { | ||||
|                   p.selected.splice(n, 1); | ||||
|                   p.onChange?.(); | ||||
|                 } | ||||
|               : undefined | ||||
|           } | ||||
|           onClick={ | ||||
|             !p.editable && entry | ||||
|               ? () => navigate(NWFilterURL(entry)) | ||||
|               : undefined | ||||
|           } | ||||
|         /> | ||||
|       ))} | ||||
|  | ||||
|       {p.editable && ( | ||||
|         <NWFilterSelectInput | ||||
|           editable={p.editable} | ||||
|           label="Attach a new filter" | ||||
|           canBeNull={false} | ||||
|           nwfilters={nwfilters} | ||||
|           value={""} | ||||
|           onChange={(f) => { | ||||
|             p.selected.push(f!); | ||||
|             p.onChange?.(); | ||||
|           }} | ||||
|         /> | ||||
|       )} | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										22
									
								
								virtweb_frontend/src/widgets/forms/NWFilterPriorityInput.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								virtweb_frontend/src/widgets/forms/NWFilterPriorityInput.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| import { ServerApi } from "../../api/ServerApi"; | ||||
| import { TextInput } from "./TextInput"; | ||||
|  | ||||
| export function NWFilterPriorityInput(p: { | ||||
|   editable: boolean; | ||||
|   label: string; | ||||
|   value?: number; | ||||
|   onChange: (priority?: number) => void; | ||||
| }): React.ReactElement { | ||||
|   return ( | ||||
|     <TextInput | ||||
|       {...p} | ||||
|       value={p.value?.toString()} | ||||
|       type="number" | ||||
|       onValueChange={(v) => { | ||||
|         p.onChange?.(v && v !== "" ? Number(v) : undefined); | ||||
|       }} | ||||
|       size={ServerApi.Config.constraints.nwfilter_priority} | ||||
|       helperText="A lower priority value is accessed before one with a higher value" | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										709
									
								
								virtweb_frontend/src/widgets/forms/NWFilterRules.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										709
									
								
								virtweb_frontend/src/widgets/forms/NWFilterRules.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,709 @@ | ||||
| import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; | ||||
| import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; | ||||
| import DeleteIcon from "@mui/icons-material/Delete"; | ||||
| import PlaylistAddIcon from "@mui/icons-material/PlaylistAdd"; | ||||
| import { | ||||
|   Button, | ||||
|   Card, | ||||
|   CardActions, | ||||
|   CardContent, | ||||
|   IconButton, | ||||
|   Paper, | ||||
|   Tooltip, | ||||
| } from "@mui/material"; | ||||
| import { | ||||
|   NWFSAllBase, | ||||
|   NWFSArpOrRARP, | ||||
|   NWFSIPBase, | ||||
|   NWFSLayer4Base, | ||||
|   NWFSMac, | ||||
|   NWFSelector, | ||||
|   NWFilterRule, | ||||
| } from "../../api/NWFilterApi"; | ||||
| import { ServerApi } from "../../api/ServerApi"; | ||||
| import { EditSection } from "./EditSection"; | ||||
| import { IPInput, IPInputWithMask } from "./IPInput"; | ||||
| import { MACInput } from "./MACInput"; | ||||
| import { NWFConnStateInput } from "./NWFConnStateInput"; | ||||
| import { NWFilterPriorityInput } from "./NWFilterPriorityInput"; | ||||
| import { PortInput } from "./PortInput"; | ||||
| import { SelectInput } from "./SelectInput"; | ||||
| import { TextInput } from "./TextInput"; | ||||
|  | ||||
| export function NWFilterRules(p: { | ||||
|   editable: boolean; | ||||
|   rules: NWFilterRule[]; | ||||
|   onChange?: () => void; | ||||
| }): React.ReactElement { | ||||
|   const addRule = () => { | ||||
|     p.rules.push({ | ||||
|       action: "drop", | ||||
|       direction: "inout", | ||||
|       selectors: [], | ||||
|     }); | ||||
|     p.onChange?.(); | ||||
|   }; | ||||
|  | ||||
|   const swapRules = (f: number, s: number) => { | ||||
|     const swap = p.rules[f]; | ||||
|     p.rules[f] = p.rules[s]; | ||||
|     p.rules[s] = swap; | ||||
|     p.onChange?.(); | ||||
|   }; | ||||
|  | ||||
|   const deleteRule = (num: number) => { | ||||
|     p.rules.splice(num, 1); | ||||
|     p.onChange?.(); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <EditSection title="Rules"> | ||||
|       {p.rules.map((r, n) => ( | ||||
|         <NWRuleEdit | ||||
|           key={n} | ||||
|           rule={r} | ||||
|           onDelete={() => { | ||||
|             deleteRule(n); | ||||
|           }} | ||||
|           onGoDown={ | ||||
|             n < p.rules.length - 1 ? () => swapRules(n, n + 1) : undefined | ||||
|           } | ||||
|           onGoUp={n > 0 ? () => swapRules(n, n - 1) : undefined} | ||||
|           {...p} | ||||
|         /> | ||||
|       ))} | ||||
|  | ||||
|       <div style={{ textAlign: "right" }}> | ||||
|         {p.editable && <Button onClick={addRule}>Add a new rule</Button>} | ||||
|       </div> | ||||
|     </EditSection> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NWRuleEdit(p: { | ||||
|   editable: boolean; | ||||
|   rule: NWFilterRule; | ||||
|   onChange?: () => void; | ||||
|   onGoUp?: () => void; | ||||
|   onGoDown?: () => void; | ||||
|   onDelete: () => void; | ||||
| }): React.ReactElement { | ||||
|   const addSelector = () => { | ||||
|     p.rule.selectors.push({ | ||||
|       type: "all", | ||||
|     }); | ||||
|     p.onChange?.(); | ||||
|   }; | ||||
|  | ||||
|   const deleteSelector = (num: number) => { | ||||
|     p.rule.selectors.splice(num, 1); | ||||
|     p.onChange?.(); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <Card style={{ margin: "30px" }} elevation={3}> | ||||
|       <CardContent> | ||||
|         <div style={{ display: "flex" }}> | ||||
|           <SelectInput | ||||
|             editable={p.editable} | ||||
|             label="Action" | ||||
|             value={p.rule.action} | ||||
|             onValueChange={(v) => { | ||||
|               p.rule.action = v as any; | ||||
|               p.onChange?.(); | ||||
|             }} | ||||
|             options={[ | ||||
|               { label: "drop", value: "drop" }, | ||||
|               { label: "reject", value: "reject" }, | ||||
|               { label: "accept", value: "accept" }, | ||||
|               { label: "return", value: "return" }, | ||||
|               { label: "continue", value: "continue" }, | ||||
|             ]} | ||||
|           /> | ||||
|           <span style={{ width: "20px" }}></span> | ||||
|           <SelectInput | ||||
|             editable={p.editable} | ||||
|             label="Direction" | ||||
|             value={p.rule.direction} | ||||
|             onValueChange={(v) => { | ||||
|               p.rule.direction = v as any; | ||||
|               p.onChange?.(); | ||||
|             }} | ||||
|             options={[ | ||||
|               { label: "in", value: "in" }, | ||||
|               { label: "out", value: "out" }, | ||||
|               { label: "inout", value: "inout" }, | ||||
|             ]} | ||||
|           /> | ||||
|           <span style={{ width: "20px" }}></span> | ||||
|           <NWFilterPriorityInput | ||||
|             {...p} | ||||
|             label="Priority" | ||||
|             value={p.rule.priority} | ||||
|             onChange={(v) => { | ||||
|               p.rule.priority = v; | ||||
|               p.onChange?.(); | ||||
|             }} | ||||
|           /> | ||||
|         </div> | ||||
|  | ||||
|         {p.rule.selectors.map((s, n) => ( | ||||
|           <NWFSelectorEdit | ||||
|             key={n} | ||||
|             editable={p.editable} | ||||
|             onChange={p.onChange} | ||||
|             selector={s} | ||||
|             onDelete={() => deleteSelector(n)} | ||||
|           /> | ||||
|         ))} | ||||
|       </CardContent> | ||||
|       <CardActions> | ||||
|         {p.editable && ( | ||||
|           <div style={{ display: "flex", width: "100%" }}> | ||||
|             <Tooltip title="Remove the rule"> | ||||
|               <IconButton color="error" onClick={p.onDelete}> | ||||
|                 <DeleteIcon /> | ||||
|               </IconButton> | ||||
|             </Tooltip> | ||||
|  | ||||
|             <span style={{ flex: 1 }}></span> | ||||
|  | ||||
|             {ServerApi.Config.constraints.nwfilter_selectors_count.max > | ||||
|               p.rule.selectors.length && ( | ||||
|               <Tooltip title="Add a selector"> | ||||
|                 <IconButton onClick={addSelector}> | ||||
|                   <PlaylistAddIcon /> | ||||
|                 </IconButton> | ||||
|               </Tooltip> | ||||
|             )} | ||||
|  | ||||
|             {p.onGoUp && ( | ||||
|               <Tooltip title="Move rule upward"> | ||||
|                 <IconButton onClick={p.onGoUp}> | ||||
|                   <ArrowUpwardIcon /> | ||||
|                 </IconButton> | ||||
|               </Tooltip> | ||||
|             )} | ||||
|  | ||||
|             {p.onGoDown && ( | ||||
|               <Tooltip title="Move rule downward"> | ||||
|                 <IconButton onClick={p.onGoDown}> | ||||
|                   <ArrowDownwardIcon /> | ||||
|                 </IconButton> | ||||
|               </Tooltip> | ||||
|             )} | ||||
|           </div> | ||||
|         )} | ||||
|       </CardActions> | ||||
|     </Card> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NWFSelectorEdit(p: { | ||||
|   editable: boolean; | ||||
|   selector: NWFSelector; | ||||
|   onDelete: () => void; | ||||
|   onChange?: () => void; | ||||
| }): React.ReactElement { | ||||
|   return ( | ||||
|     <Paper elevation={10} style={{ padding: "10px" }}> | ||||
|       <div style={{ display: "flex", width: "100%" }}> | ||||
|         <div style={{ flex: 1 }}> | ||||
|           <SelectInput | ||||
|             editable={p.editable} | ||||
|             label="Type" | ||||
|             onValueChange={(v) => { | ||||
|               p.selector.type = v! as any; | ||||
|               p.onChange?.(); | ||||
|             }} | ||||
|             value={p.selector.type} | ||||
|             options={[ | ||||
|               { label: "MAC (Ethernet)", value: "mac" }, | ||||
|  | ||||
|               { label: "ARP", value: "arp" }, | ||||
|               { label: "RARP", value: "rarp" }, | ||||
|  | ||||
|               { label: "IPv4", value: "ipv4" }, | ||||
|               { label: "IPv6", value: "ipv6" }, | ||||
|  | ||||
|               { label: "TCP over IPv4", value: "tcp" }, | ||||
|               { label: "UDP over IPv4", value: "udp" }, | ||||
|               { label: "SCTP over IPv4", value: "sctp" }, | ||||
|               { label: "ICMPv4", value: "icmp" }, | ||||
|  | ||||
|               { label: "All over IPv4", value: "all" }, | ||||
|  | ||||
|               { label: "TCP over IPv6", value: "tcpipv6" }, | ||||
|               { label: "UDP over IPv6", value: "udpipv6" }, | ||||
|               { label: "SCTP over IPv6", value: "sctpipv6" }, | ||||
|               { label: "ICMPv6", value: "icmpipv6" }, | ||||
|  | ||||
|               { label: "All over IPv6", value: "allipv6" }, | ||||
|             ]} | ||||
|           /> | ||||
|  | ||||
|           {p.selector.type === "mac" && ( | ||||
|             <NWFSelectorMac {...p} selector={p.selector} /> | ||||
|           )} | ||||
|  | ||||
|           {(p.selector.type === "arp" || p.selector.type === "rarp") && ( | ||||
|             <NWFSelectorArp {...p} selector={p.selector} /> | ||||
|           )} | ||||
|  | ||||
|           {p.selector.type === "ipv4" && ( | ||||
|             <NWFSelectorIP {...p} selector={p.selector} version={4} /> | ||||
|           )} | ||||
|  | ||||
|           {p.selector.type === "ipv6" && ( | ||||
|             <NWFSelectorIP {...p} selector={p.selector} version={6} /> | ||||
|           )} | ||||
|  | ||||
|           {(p.selector.type === "tcp" || | ||||
|             p.selector.type === "udp" || | ||||
|             p.selector.type === "sctp" || | ||||
|             p.selector.type === "icmp") && ( | ||||
|             <NWFSelectorLayer4 {...p} selector={p.selector} version={4} /> | ||||
|           )} | ||||
|  | ||||
|           {p.selector.type === "all" && ( | ||||
|             <NWFSelectorAll {...p} selector={p.selector} version={4} /> | ||||
|           )} | ||||
|  | ||||
|           {(p.selector.type === "tcpipv6" || | ||||
|             p.selector.type === "udpipv6" || | ||||
|             p.selector.type === "sctpipv6" || | ||||
|             p.selector.type === "icmpipv6") && ( | ||||
|             <NWFSelectorLayer4 {...p} selector={p.selector} version={6} /> | ||||
|           )} | ||||
|  | ||||
|           {p.selector.type === "allipv6" && ( | ||||
|             <NWFSelectorAll {...p} selector={p.selector} version={6} /> | ||||
|           )} | ||||
|  | ||||
|           <TextInput | ||||
|             editable={p.editable} | ||||
|             label="Comment" | ||||
|             value={p.selector.comment} | ||||
|             onValueChange={(v) => { | ||||
|               p.selector.comment = v; | ||||
|               p.onChange?.(); | ||||
|             }} | ||||
|             size={ServerApi.Config.constraints.nwfilter_comment_size} | ||||
|           /> | ||||
|         </div> | ||||
|  | ||||
|         {p.editable && ( | ||||
|           <div style={{ display: "flex", justifyContent: "center" }}> | ||||
|             <Tooltip title="Remove the selector"> | ||||
|               <IconButton color="error" onClick={p.onDelete}> | ||||
|                 <DeleteIcon /> | ||||
|               </IconButton> | ||||
|             </Tooltip> | ||||
|           </div> | ||||
|         )} | ||||
|       </div> | ||||
|     </Paper> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| interface SpecificSelectorEditor<E> { | ||||
|   editable: boolean; | ||||
|   selector: E; | ||||
|   onChange?: () => void; | ||||
| } | ||||
|  | ||||
| interface SpecificSelectorEditorWithIPVersion<E> | ||||
|   extends SpecificSelectorEditor<E> { | ||||
|   version: 4 | 6; | ||||
| } | ||||
|  | ||||
| function NWFSelectorMac( | ||||
|   p: SpecificSelectorEditor<NWFSMac> | ||||
| ): React.ReactElement { | ||||
|   return ( | ||||
|     <> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Src mac address" | ||||
|         value={p.selector.src_mac_addr} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.src_mac_addr = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Src mac mask" | ||||
|         value={p.selector.src_mac_mask} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.src_mac_mask = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Dst mac address" | ||||
|         value={p.selector.dst_mac_addr} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.dst_mac_addr = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Dst mac mask" | ||||
|         value={p.selector.dst_mac_mask} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.dst_mac_mask = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NWFSelectorArp( | ||||
|   p: SpecificSelectorEditor<NWFSArpOrRARP> | ||||
| ): React.ReactElement { | ||||
|   return ( | ||||
|     <> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Src mac address" | ||||
|         value={p.selector.srcmacaddr} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.srcmacaddr = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Src mac mask" | ||||
|         value={p.selector.srcmacmask} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.srcmacmask = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Dst mac address" | ||||
|         value={p.selector.dstmacaddr} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.dstmacaddr = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Dst mac mask" | ||||
|         value={p.selector.dstmacmask} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.dstmacmask = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInputWithMask | ||||
|         {...p} | ||||
|         label="ARP src ip" | ||||
|         ip={p.selector.arpsrcipaddr} | ||||
|         mask={p.selector.arpsrcipmask} | ||||
|         version={4} | ||||
|         onValueChange={(ip, mask) => { | ||||
|           p.selector.arpsrcipaddr = ip; | ||||
|           p.selector.arpsrcipmask = mask; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInputWithMask | ||||
|         {...p} | ||||
|         label="ARP dst ip" | ||||
|         ip={p.selector.arpdstipaddr} | ||||
|         mask={p.selector.arpdstipmask} | ||||
|         version={4} | ||||
|         onValueChange={(ip, mask) => { | ||||
|           p.selector.arpdstipaddr = ip; | ||||
|           p.selector.arpdstipmask = mask; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NWFSelectorIP( | ||||
|   p: SpecificSelectorEditorWithIPVersion<NWFSIPBase> | ||||
| ): React.ReactElement { | ||||
|   return ( | ||||
|     <> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Src mac address" | ||||
|         value={p.selector.srcmacaddr} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.srcmacaddr = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Src mac mask" | ||||
|         value={p.selector.srcmacmask} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.srcmacmask = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Dst mac address" | ||||
|         value={p.selector.dstmacaddr} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.dstmacaddr = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Dst mac mask" | ||||
|         value={p.selector.dstmacmask} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.dstmacmask = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInputWithMask | ||||
|         {...p} | ||||
|         label="Source IP address / mask" | ||||
|         ip={p.selector.srcipaddr} | ||||
|         mask={p.selector.srcipmask} | ||||
|         version={p.version} | ||||
|         onValueChange={(ip, mask) => { | ||||
|           p.selector.srcipaddr = ip; | ||||
|           p.selector.srcipmask = mask; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInputWithMask | ||||
|         {...p} | ||||
|         label="Destination IP address / mask" | ||||
|         ip={p.selector.dstipaddr} | ||||
|         mask={p.selector.dstipmask} | ||||
|         version={p.version} | ||||
|         onValueChange={(ip, mask) => { | ||||
|           p.selector.dstipaddr = ip; | ||||
|           p.selector.dstipmask = mask; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NWFSelectorLayer4( | ||||
|   p: SpecificSelectorEditorWithIPVersion<NWFSLayer4Base> | ||||
| ): React.ReactElement { | ||||
|   return ( | ||||
|     <> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Src mac address" | ||||
|         value={p.selector.srcmacaddr} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.srcmacaddr = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInputWithMask | ||||
|         {...p} | ||||
|         label="Source IP address / mask" | ||||
|         ip={p.selector.srcipaddr} | ||||
|         mask={p.selector.srcipmask} | ||||
|         version={p.version} | ||||
|         onValueChange={(ip, mask) => { | ||||
|           p.selector.srcipaddr = ip; | ||||
|           p.selector.srcipmask = mask; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInputWithMask | ||||
|         {...p} | ||||
|         label="Destination IP address / mask" | ||||
|         ip={p.selector.dstipaddr} | ||||
|         mask={p.selector.dstipmask} | ||||
|         version={p.version} | ||||
|         onValueChange={(ip, mask) => { | ||||
|           p.selector.dstipaddr = ip; | ||||
|           p.selector.dstipmask = mask; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInput | ||||
|         {...p} | ||||
|         label="Source IP from" | ||||
|         value={p.selector.srcipfrom} | ||||
|         onValueChange={(ip) => { | ||||
|           p.selector.srcipfrom = ip; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInput | ||||
|         {...p} | ||||
|         label="Source IP to" | ||||
|         value={p.selector.srcipto} | ||||
|         onValueChange={(ip) => { | ||||
|           p.selector.srcipto = ip; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInput | ||||
|         {...p} | ||||
|         label="Destination IP from" | ||||
|         value={p.selector.dstipfrom} | ||||
|         onValueChange={(ip) => { | ||||
|           p.selector.dstipfrom = ip; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInput | ||||
|         {...p} | ||||
|         label="Destination IP to" | ||||
|         value={p.selector.dstipto} | ||||
|         onValueChange={(ip) => { | ||||
|           p.selector.dstipto = ip; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <PortInput | ||||
|         {...p} | ||||
|         label="Source port start" | ||||
|         value={p.selector.srcportstart} | ||||
|         onChange={(port) => { | ||||
|           p.selector.srcportstart = port; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <PortInput | ||||
|         {...p} | ||||
|         label="Source port end" | ||||
|         value={p.selector.srcportend} | ||||
|         onChange={(port) => { | ||||
|           p.selector.srcportend = port; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <PortInput | ||||
|         {...p} | ||||
|         label="Destination port start" | ||||
|         value={p.selector.dstportstart} | ||||
|         onChange={(port) => { | ||||
|           p.selector.dstportstart = port; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <PortInput | ||||
|         {...p} | ||||
|         label="Destination port end" | ||||
|         value={p.selector.dstportend} | ||||
|         onChange={(port) => { | ||||
|           p.selector.dstportend = port; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <NWFConnStateInput | ||||
|         {...p} | ||||
|         value={p.selector.state} | ||||
|         onChange={(v) => { | ||||
|           p.selector.state = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NWFSelectorAll( | ||||
|   p: SpecificSelectorEditorWithIPVersion<NWFSAllBase> | ||||
| ): React.ReactElement { | ||||
|   return ( | ||||
|     <> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Src mac address" | ||||
|         value={p.selector.srcmacaddr} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.srcmacaddr = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInputWithMask | ||||
|         {...p} | ||||
|         label="Source IP address / mask" | ||||
|         ip={p.selector.srcipaddr} | ||||
|         mask={p.selector.srcipmask} | ||||
|         version={p.version} | ||||
|         onValueChange={(ip, mask) => { | ||||
|           p.selector.srcipaddr = ip; | ||||
|           p.selector.srcipmask = mask; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInputWithMask | ||||
|         {...p} | ||||
|         label="Destination IP address / mask" | ||||
|         ip={p.selector.dstipaddr} | ||||
|         mask={p.selector.dstipmask} | ||||
|         version={p.version} | ||||
|         onValueChange={(ip, mask) => { | ||||
|           p.selector.dstipaddr = ip; | ||||
|           p.selector.dstipmask = mask; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInput | ||||
|         {...p} | ||||
|         label="Source IP from" | ||||
|         value={p.selector.srcipfrom} | ||||
|         onValueChange={(ip) => { | ||||
|           p.selector.srcipfrom = ip; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInput | ||||
|         {...p} | ||||
|         label="Source IP to" | ||||
|         value={p.selector.srcipto} | ||||
|         onValueChange={(ip) => { | ||||
|           p.selector.srcipto = ip; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInput | ||||
|         {...p} | ||||
|         label="Destination IP from" | ||||
|         value={p.selector.dstipfrom} | ||||
|         onValueChange={(ip) => { | ||||
|           p.selector.dstipfrom = ip; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInput | ||||
|         {...p} | ||||
|         label="Destination IP to" | ||||
|         value={p.selector.dstipto} | ||||
|         onValueChange={(ip) => { | ||||
|           p.selector.dstipto = ip; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <NWFConnStateInput | ||||
|         {...p} | ||||
|         value={p.selector.state} | ||||
|         onChange={(v) => { | ||||
|           p.selector.state = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										63
									
								
								virtweb_frontend/src/widgets/forms/NWFilterSelectInput.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								virtweb_frontend/src/widgets/forms/NWFilterSelectInput.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| import { Autocomplete, TextField } from "@mui/material"; | ||||
| import React from "react"; | ||||
| import { useNavigate } from "react-router-dom"; | ||||
| import { NWFilter, NWFilterURL } from "../../api/NWFilterApi"; | ||||
| import { NWFilterItem } from "../nwfilter/NWFilterItem"; | ||||
|  | ||||
| export function NWFilterSelectInput(p: { | ||||
|   editable: boolean; | ||||
|   label?: string; | ||||
|   nwfilters: NWFilter[]; | ||||
|   value?: string; | ||||
|   onChange?: (name?: string) => void; | ||||
|   canBeNull: boolean; | ||||
| }): React.ReactElement { | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const [open, setOpen] = React.useState(false); | ||||
|  | ||||
|   const selectedValue = p.nwfilters.find((o) => o.name === p.value); | ||||
|   if (!p.editable && !selectedValue) return <></>; | ||||
|  | ||||
|   if (selectedValue) | ||||
|     return ( | ||||
|       <NWFilterItem | ||||
|         value={selectedValue} | ||||
|         onDelete={p.editable ? () => p.onChange?.(undefined) : undefined} | ||||
|         onClick={ | ||||
|           !p.editable && selectedValue | ||||
|             ? () => navigate(NWFilterURL(selectedValue)) | ||||
|             : undefined | ||||
|         } | ||||
|       /> | ||||
|     ); | ||||
|  | ||||
|   return ( | ||||
|     <Autocomplete | ||||
|       open={open} | ||||
|       onOpen={() => { | ||||
|         setOpen(true); | ||||
|       }} | ||||
|       onClose={() => { | ||||
|         setOpen(false); | ||||
|       }} | ||||
|       readOnly={!p.editable} | ||||
|       options={[...(p.canBeNull ? [undefined] : []), ...p.nwfilters]} | ||||
|       getOptionLabel={(o) => o?.name ?? "Unspecified"} | ||||
|       value={selectedValue} | ||||
|       renderInput={(params) => ( | ||||
|         <TextField {...params} variant="standard" label={p.label} /> | ||||
|       )} | ||||
|       renderOption={(_props, option, _state) => ( | ||||
|         <NWFilterItem | ||||
|           dense | ||||
|           onClick={() => { | ||||
|             p.onChange?.(option?.name); | ||||
|             setOpen(false); | ||||
|           }} | ||||
|           value={option} | ||||
|         /> | ||||
|       )} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| @@ -14,11 +14,11 @@ import { | ||||
| import { DHCPConfig, DHCPHost } from "../../api/NetworksApi"; | ||||
| import { ServerApi } from "../../api/ServerApi"; | ||||
| import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider"; | ||||
| import { IPInput } from "../forms/IPInput"; | ||||
| import { MACInput } from "../forms/MACInput"; | ||||
| import { TextInput } from "../forms/TextInput"; | ||||
| import { IPInput } from "./IPInput"; | ||||
| import { MACInput } from "./MACInput"; | ||||
| import { TextInput } from "./TextInput"; | ||||
| 
 | ||||
| export function DHCPHostReservations(p: { | ||||
| export function NetDHCPHostReservations(p: { | ||||
|   editable: boolean; | ||||
|   dhcp: DHCPConfig; | ||||
|   version: 4 | 6; | ||||
| @@ -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 && ( | ||||
							
								
								
									
										28
									
								
								virtweb_frontend/src/widgets/forms/PortInput.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								virtweb_frontend/src/widgets/forms/PortInput.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| import { TextInput } from "./TextInput"; | ||||
|  | ||||
| export function PortInput(p: { | ||||
|   editable: boolean; | ||||
|   label: string; | ||||
|   value?: number; | ||||
|   onChange: (value: number | undefined) => void; | ||||
| }): React.ReactElement { | ||||
|   return ( | ||||
|     <TextInput | ||||
|       {...p} | ||||
|       value={p.value?.toString() ?? ""} | ||||
|       type="number" | ||||
|       onValueChange={(v) => { | ||||
|         p.onChange?.(sanitizePort(v)); | ||||
|       }} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function sanitizePort(port?: string): number | undefined { | ||||
|   if (port === undefined) return undefined; | ||||
|   const val = Number(port); | ||||
|  | ||||
|   if (val < 0) return 0; | ||||
|   if (val > 65535) return 65535; | ||||
|   return val; | ||||
| } | ||||
| @@ -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,8 @@ export function TextInput(p: { | ||||
|   minRows?: number; | ||||
|   maxRows?: number; | ||||
|   type?: React.HTMLInputTypeAttribute; | ||||
|   style?: React.CSSProperties; | ||||
|   helperText?: string; | ||||
| }): React.ReactElement { | ||||
|   if (!p.editable && (p.value ?? "") === "") return <></>; | ||||
|  | ||||
| @@ -48,12 +50,12 @@ 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} | ||||
|       error={valueError !== undefined} | ||||
|       helperText={valueError} | ||||
|       helperText={valueError ?? p.helperText} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -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,31 @@ 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 { EditSection } from "./EditSection"; | ||||
| import { MACInput } from "./MACInput"; | ||||
| import { NWFilterSelectInput } from "./NWFilterSelectInput"; | ||||
| import { SelectInput } from "./SelectInput"; | ||||
| import { VMNetworkFilterParameters } from "./VMNetworkFilterParameters"; | ||||
|  | ||||
| export function VMNetworksList(p: { | ||||
|   vm: VMInfo; | ||||
|   onChange?: () => void; | ||||
|   editable: boolean; | ||||
|   networksList: NetworkInfo[]; | ||||
|   networkFiltersList: NWFilter[]; | ||||
| }): React.ReactElement { | ||||
|   const addNew = () => { | ||||
|     p.vm.networks.push({ | ||||
| @@ -34,22 +40,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 +72,7 @@ function NetworkInfoWidget(p: { | ||||
|   onChange?: () => void; | ||||
|   removeFromList: () => void; | ||||
|   networksList: NetworkInfo[]; | ||||
|   networkFiltersList: NWFilter[]; | ||||
| }): React.ReactElement { | ||||
|   const confirm = useConfirm(); | ||||
|   const deleteNetwork = async () => { | ||||
| @@ -137,28 +150,58 @@ 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 */} | ||||
|             <NWFilterSelectInput | ||||
|               editable={p.editable} | ||||
|               label="Network filter" | ||||
|               value={p.network.nwfilterref?.name} | ||||
|               onChange={(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?.(); | ||||
|               }} | ||||
|               canBeNull={true} | ||||
|               nwfilters={p.networkFiltersList} | ||||
|             /> | ||||
|  | ||||
|             {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, | ||||
|             }; | ||||
|           }), | ||||
|         ]} | ||||
|       /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -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 { NetDHCPHostReservations } from "../forms/NetDHCPHostReservations"; | ||||
| import { XMLAsyncWidget } from "../XMLWidget"; | ||||
|  | ||||
| interface DetailsProps { | ||||
|   net: NetworkInfo; | ||||
| @@ -19,10 +23,10 @@ interface DetailsProps { | ||||
| } | ||||
|  | ||||
| export function NetworkDetails(p: DetailsProps): React.ReactElement { | ||||
|   const [cardsList, setCardsList] = React.useState<string[] | any>(); | ||||
|   const [nicsList, setNicsList] = React.useState<string[] | any>(); | ||||
|  | ||||
|   const load = async () => { | ||||
|     setCardsList(await ServerApi.GetNetworksList()); | ||||
|     setNicsList(await ServerApi.GetNetworksList()); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
| @@ -30,14 +34,65 @@ export function NetworkDetails(p: DetailsProps): React.ReactElement { | ||||
|       loadKey={"1"} | ||||
|       load={load} | ||||
|       errMsg="Failed to load the list of host network cards!" | ||||
|       build={() => <NetworkDetailsInner cardsList={cardsList} {...p} />} | ||||
|       build={() => <NetworkDetailsInner nicsList={nicsList} {...p} />} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NetworkDetailsInner( | ||||
|   p: DetailsProps & { cardsList: string[] } | ||||
| ): React.ReactElement { | ||||
| enum NetTab { | ||||
|   General = 0, | ||||
|   IPv4, | ||||
|   IPv6, | ||||
|   XML, | ||||
|   Danger, | ||||
| } | ||||
|  | ||||
| type DetailsInnerProps = DetailsProps & { nicsList: 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 */} | ||||
| @@ -121,7 +176,7 @@ function NetworkDetailsInner( | ||||
|             value={p.net.device} | ||||
|             options={[ | ||||
|               { label: "Default interface", value: undefined }, | ||||
|               ...p.cardsList.map((d) => { | ||||
|               ...p.nicsList.map((d) => { | ||||
|                 return { label: d, value: d }; | ||||
|               }), | ||||
|             ]} | ||||
| @@ -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"> | ||||
|           <NetDHCPHostReservations | ||||
|             {...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> | ||||
|   ); | ||||
| } | ||||
|   | ||||
							
								
								
									
										218
									
								
								virtweb_frontend/src/widgets/nwfilter/NWFilterDetails.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								virtweb_frontend/src/widgets/nwfilter/NWFilterDetails.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,218 @@ | ||||
| import { Button, Grid } from "@mui/material"; | ||||
| import React, { ReactElement } from "react"; | ||||
| import { useNavigate } from "react-router-dom"; | ||||
| import { | ||||
|   NWFilter, | ||||
|   NWFilterApi, | ||||
|   NWFilterIsBuiltin, | ||||
| } from "../../api/NWFilterApi"; | ||||
| 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 { XMLAsyncWidget } from "../XMLWidget"; | ||||
| import { EditSection } from "../forms/EditSection"; | ||||
| import { TextInput } from "../forms/TextInput"; | ||||
| import { ServerApi } from "../../api/ServerApi"; | ||||
| import { SelectInput } from "../forms/SelectInput"; | ||||
| import { NWFSelectReferencedFilters } from "../forms/NWFSelectReferencedFilters"; | ||||
| import { NWFilterRules } from "../forms/NWFilterRules"; | ||||
| import { NWFilterPriorityInput } from "../forms/NWFilterPriorityInput"; | ||||
|  | ||||
| interface DetailsProps { | ||||
|   nwfilter: NWFilter; | ||||
|   editable: boolean; | ||||
|   onChange?: () => void; | ||||
| } | ||||
|  | ||||
| export function NWFilterDetails(p: DetailsProps): ReactElement { | ||||
|   const [nwFiltersList, setNwFiltersList] = React.useState<NWFilter[] | any>(); | ||||
|  | ||||
|   const load = async () => { | ||||
|     setNwFiltersList(await NWFilterApi.GetList()); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <AsyncWidget | ||||
|       loadKey={p.nwfilter.uuid} | ||||
|       load={load} | ||||
|       errMsg="Failed to load the list of network filters!" | ||||
|       build={() => ( | ||||
|         <NetworkFilterDetailsInner nwFiltersList={nwFiltersList} {...p} /> | ||||
|       )} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| type InnerDetailsProps = DetailsProps & { nwFiltersList: NWFilter[] }; | ||||
|  | ||||
| enum NetFilterTab { | ||||
|   General = 0, | ||||
|   Rules, | ||||
|   XML, | ||||
|   Danger, | ||||
| } | ||||
|  | ||||
| export function NetworkFilterDetailsInner( | ||||
|   p: InnerDetailsProps | ||||
| ): React.ReactElement { | ||||
|   const [currTab, setCurrTab] = React.useState(NetFilterTab.General); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <TabsWidget | ||||
|         currTab={currTab} | ||||
|         onTabChange={setCurrTab} | ||||
|         options={[ | ||||
|           { label: "General", value: NetFilterTab.General, visible: true }, | ||||
|           { | ||||
|             label: "Rules", | ||||
|             value: NetFilterTab.Rules, | ||||
|             visible: p.editable || p.nwfilter.rules.length > 0, | ||||
|           }, | ||||
|  | ||||
|           { | ||||
|             label: "XML", | ||||
|             value: NetFilterTab.XML, | ||||
|             visible: !p.editable, | ||||
|           }, | ||||
|           { | ||||
|             label: "Danger zone", | ||||
|             value: NetFilterTab.Danger, | ||||
|             color: "red", | ||||
|             visible: !p.editable && !NWFilterIsBuiltin(p.nwfilter), | ||||
|           }, | ||||
|         ]} | ||||
|       /> | ||||
|  | ||||
|       {currTab === NetFilterTab.General && ( | ||||
|         <NetworkFilterDetailsTabGeneral {...p} /> | ||||
|       )} | ||||
|       {currTab === NetFilterTab.Rules && ( | ||||
|         <NetworkFilterDetailsTabRules {...p} /> | ||||
|       )} | ||||
|       {currTab === NetFilterTab.XML && <NetworkFilterDetailsTabXML {...p} />} | ||||
|       {currTab === NetFilterTab.Danger && ( | ||||
|         <NetworkFilterDetailsTabDanger {...p} /> | ||||
|       )} | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NetworkFilterDetailsTabGeneral( | ||||
|   p: InnerDetailsProps | ||||
| ): React.ReactElement { | ||||
|   return ( | ||||
|     <Grid container spacing={2}> | ||||
|       {/* Metadata section */} | ||||
|       <EditSection title="Metadata"> | ||||
|         <TextInput | ||||
|           label="Name" | ||||
|           editable={p.editable} | ||||
|           value={p.nwfilter.name} | ||||
|           onValueChange={(v) => { | ||||
|             p.nwfilter.name = v ?? ""; | ||||
|             p.onChange?.(); | ||||
|           }} | ||||
|           checkValue={(v) => /^[a-zA-Z0-9\_\-]+$/.test(v)} | ||||
|           size={ServerApi.Config.constraints.nwfilter_name_size} | ||||
|         /> | ||||
|  | ||||
|         <TextInput label="UUID" editable={false} value={p.nwfilter.uuid} /> | ||||
|  | ||||
|         <SelectInput | ||||
|           label="Chain" | ||||
|           editable={p.editable} | ||||
|           value={p.nwfilter.chain?.protocol} | ||||
|           onValueChange={(v) => { | ||||
|             p.nwfilter.chain = v ? { protocol: v } : undefined; | ||||
|             p.onChange?.(); | ||||
|           }} | ||||
|           options={ServerApi.Config.nwfilter_chains.map((c) => { | ||||
|             return { label: c, value: c }; | ||||
|           })} | ||||
|         /> | ||||
|  | ||||
|         <NWFilterPriorityInput | ||||
|           {...p} | ||||
|           label="Priority" | ||||
|           value={p.nwfilter.priority} | ||||
|           onChange={(pri) => { | ||||
|             p.nwfilter.priority = pri; | ||||
|             p.onChange?.(); | ||||
|           }} | ||||
|         /> | ||||
|       </EditSection> | ||||
|  | ||||
|       {/* Referenced filters */} | ||||
|       {(p.editable || p.nwfilter.join_filters.length > 0) && ( | ||||
|         <EditSection title="Referenced filters"> | ||||
|           <NWFSelectReferencedFilters | ||||
|             selected={p.nwfilter.join_filters} | ||||
|             excludedFilters={[p.nwfilter.name]} | ||||
|             {...p} | ||||
|           /> | ||||
|         </EditSection> | ||||
|       )} | ||||
|     </Grid> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NetworkFilterDetailsTabRules( | ||||
|   p: InnerDetailsProps | ||||
| ): React.ReactElement { | ||||
|   return ( | ||||
|     <NWFilterRules | ||||
|       editable={p.editable} | ||||
|       rules={p.nwfilter.rules} | ||||
|       onChange={p.onChange} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NetworkFilterDetailsTabXML(p: InnerDetailsProps): React.ReactElement { | ||||
|   return ( | ||||
|     <XMLAsyncWidget | ||||
|       errMsg="Failed to load network filter XML definition!" | ||||
|       identifier={p.nwfilter.uuid!} | ||||
|       load={() => NWFilterApi.GetSingleXML(p.nwfilter.uuid!)} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NetworkFilterDetailsTabDanger( | ||||
|   p: InnerDetailsProps | ||||
| ): 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 filter?", | ||||
|           `Delete network filter ${p.nwfilter.name}`, | ||||
|           "Delete" | ||||
|         )) | ||||
|       ) | ||||
|         return; | ||||
|  | ||||
|       await NWFilterApi.Delete(p.nwfilter); | ||||
|  | ||||
|       navigate("/nwfilter"); | ||||
|       snackbar("The network filter was successfully deleted!"); | ||||
|     } catch (e) { | ||||
|       console.error(e); | ||||
|       alert(`Failed to delete the network filter!\n${e}`); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <Button color="error" onClick={requestDelete}> | ||||
|       Delete this network filter | ||||
|     </Button> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										71
									
								
								virtweb_frontend/src/widgets/nwfilter/NWFilterItem.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								virtweb_frontend/src/widgets/nwfilter/NWFilterItem.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | ||||
| import { mdiSecurityNetwork } from "@mdi/js"; | ||||
| import Icon from "@mdi/react"; | ||||
| import DeleteIcon from "@mui/icons-material/Delete"; | ||||
|  | ||||
| import { | ||||
|   Avatar, | ||||
|   IconButton, | ||||
|   ListItem, | ||||
|   ListItemAvatar, | ||||
|   ListItemButton, | ||||
|   ListItemText, | ||||
| } from "@mui/material"; | ||||
| import { NWFilter } from "../../api/NWFilterApi"; | ||||
|  | ||||
| export function NWFilterItem(p: { | ||||
|   value?: NWFilter; | ||||
|   onClick?: () => void; | ||||
|   dense?: boolean; | ||||
|   onDelete?: () => void; | ||||
| }): React.ReactElement { | ||||
|   const specs = []; | ||||
|   if (p.value) { | ||||
|     if (p.value.rules.length === 1) specs.push(`1 rule`); | ||||
|     else if (p.value.rules.length > 1) | ||||
|       specs.push(`${p.value.rules.length} rules`); | ||||
|  | ||||
|     if (p.value.join_filters.length === 1) specs.push(`1 joint filter`); | ||||
|     else if (p.value.join_filters.length > 1) | ||||
|       specs.push(`${p.value.join_filters.length} joint filters`); | ||||
|  | ||||
|     if (p.value.priority) specs.push(`priority: ${p.value.priority}`); | ||||
|   } | ||||
|   const inner = ( | ||||
|     <> | ||||
|       <ListItemAvatar> | ||||
|         <Avatar> | ||||
|           <Icon path={mdiSecurityNetwork} /> | ||||
|         </Avatar> | ||||
|       </ListItemAvatar> | ||||
|       <ListItemText | ||||
|         primary={ | ||||
|           p.value | ||||
|             ? `${p.value.name} (${p.value.chain?.protocol ?? "unspecified"})` | ||||
|             : "Unspecified" | ||||
|         } | ||||
|         secondary={specs.join(" / ")} | ||||
|       /> | ||||
|     </> | ||||
|   ); | ||||
|  | ||||
|   if (p.onClick) | ||||
|     return ( | ||||
|       <ListItemButton onClick={p.onClick} dense={p.dense}> | ||||
|         {inner} | ||||
|       </ListItemButton> | ||||
|     ); | ||||
|  | ||||
|   return ( | ||||
|     <ListItem | ||||
|       secondaryAction={ | ||||
|         p.onDelete ? ( | ||||
|           <IconButton onClick={p.onDelete}> | ||||
|             <DeleteIcon /> | ||||
|           </IconButton> | ||||
|         ) : undefined | ||||
|       } | ||||
|     > | ||||
|       {inner} | ||||
|     </ListItem> | ||||
|   ); | ||||
| } | ||||
| @@ -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