From d8a6b58c52d3ef35dc76f01bcef417d906b24ae4 Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Sat, 23 Dec 2023 18:12:46 +0100 Subject: [PATCH] Automatically backup source network and VM configuration --- virtweb_backend/src/actors/libvirt_actor.rs | 48 +++++++++++--- virtweb_backend/src/app_config.rs | 12 ++++ .../src/controllers/network_controller.rs | 8 +-- .../src/controllers/vm_controller.rs | 8 +-- virtweb_backend/src/libvirt_client.rs | 18 ++++-- .../src/libvirt_rest_structures.rs | 62 ++++++++++--------- virtweb_backend/src/main.rs | 1 + 7 files changed, 106 insertions(+), 51 deletions(-) diff --git a/virtweb_backend/src/actors/libvirt_actor.rs b/virtweb_backend/src/actors/libvirt_actor.rs index aac4de1..55e548b 100644 --- a/virtweb_backend/src/actors/libvirt_actor.rs +++ b/virtweb_backend/src/actors/libvirt_actor.rs @@ -115,17 +115,24 @@ impl Handler for LibVirtActor { #[derive(Message)] #[rtype(result = "anyhow::Result")] -pub struct DefineDomainReq(pub DomainXML); +pub struct DefineDomainReq(pub VMInfo, pub DomainXML); impl Handler for LibVirtActor { type Result = anyhow::Result; - 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.into_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 +162,12 @@ impl Handler 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 +373,27 @@ impl Handler for LibVirtActor { #[derive(Message)] #[rtype(result = "anyhow::Result")] -pub struct DefineNetwork(pub NetworkXML); +pub struct DefineNetwork(pub NetworkInfo, pub NetworkXML); impl Handler for LibVirtActor { type Result = anyhow::Result; - 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.into_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) } } @@ -437,7 +457,15 @@ impl Handler 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(()) } } diff --git a/virtweb_backend/src/app_config.rs b/virtweb_backend/src/app_config.rs index 9181120..d519b75 100644 --- a/virtweb_backend/src/app_config.rs +++ b/virtweb_backend/src/app_config.rs @@ -240,6 +240,18 @@ 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")) + } } #[derive(Debug, Clone, serde::Serialize)] diff --git a/virtweb_backend/src/controllers/network_controller.rs b/virtweb_backend/src/controllers/network_controller.rs index 1382630..46d5fc8 100644 --- a/virtweb_backend/src/controllers/network_controller.rs +++ b/virtweb_backend/src/controllers/network_controller.rs @@ -10,7 +10,7 @@ pub struct NetworkID { /// Create a new network pub async fn create(client: LibVirtReq, req: web::Json) -> 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) -> 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, body: web::Json, ) -> 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}")) diff --git a/virtweb_backend/src/controllers/vm_controller.rs b/virtweb_backend/src/controllers/vm_controller.rs index d169cb4..38e960e 100644 --- a/virtweb_backend/src/controllers/vm_controller.rs +++ b/virtweb_backend/src/controllers/vm_controller.rs @@ -20,7 +20,7 @@ struct VMUuid { /// Create a new VM pub async fn create(client: LibVirtReq, req: web::Json) -> 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 +29,7 @@ pub async fn create(client: LibVirtReq, req: web::Json) -> 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 +111,13 @@ pub async fn update( id: web::Path, req: web::Json, ) -> HttpResult { - let mut domain = req.0.to_domain().map_err(|e| { + let mut domain = req.0.as_tomain().map_err(|e| { log::error!("Failed to extract domain info! {e}"); 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}"))); } diff --git a/virtweb_backend/src/libvirt_client.rs b/virtweb_backend/src/libvirt_client.rs index 56d11b3..9f7443a 100644 --- a/virtweb_backend/src/libvirt_client.rs +++ b/virtweb_backend/src/libvirt_client.rs @@ -1,7 +1,7 @@ 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_rest_structures::{HypervisorInfo, NetworkInfo, VMInfo}; use actix::Addr; #[derive(Clone)] @@ -36,8 +36,10 @@ impl LibVirtClient { } /// Update a domain - pub async fn update_domain(&self, xml: DomainXML) -> anyhow::Result { - self.0.send(libvirt_actor::DefineDomainReq(xml)).await? + pub async fn update_domain(&self, vm_def: VMInfo, xml: DomainXML) -> anyhow::Result { + self.0 + .send(libvirt_actor::DefineDomainReq(vm_def, xml)) + .await? } /// Delete a domain @@ -100,8 +102,14 @@ impl LibVirtClient { } /// Update a network configuration - pub async fn update_network(&self, network: NetworkXML) -> anyhow::Result { - self.0.send(libvirt_actor::DefineNetwork(network)).await? + pub async fn update_network( + &self, + net_def: NetworkInfo, + network: NetworkXML, + ) -> anyhow::Result { + self.0 + .send(libvirt_actor::DefineNetwork(net_def, network)) + .await? } /// Get the full list of networks diff --git a/virtweb_backend/src/libvirt_rest_structures.rs b/virtweb_backend/src/libvirt_rest_structures.rs index c5ea09d..99a6856 100644 --- a/virtweb_backend/src/libvirt_rest_structures.rs +++ b/virtweb_backend/src/libvirt_rest_structures.rs @@ -108,7 +108,7 @@ pub struct VMInfo { impl VMInfo { /// Turn this VM into a domain - pub fn to_domain(self) -> anyhow::Result { + pub fn as_tomain(&self) -> anyhow::Result { if !regex!("^[a-zA-Z0-9]+$").is_match(&self.name) { return Err(StructureExtraction("VM name is invalid!").into()); } @@ -207,7 +207,7 @@ impl VMInfo { } // Apply disks configuration - for disk in self.disks { + for disk in &self.disks { disk.check_config()?; disk.apply_config(uuid)?; @@ -242,28 +242,34 @@ impl VMInfo { } let mut networks = vec![]; - for n in self.networks { - networks.push(match n.r#type { + for n in &self.networks { + networks.push(match &n.r#type { NetworkType::UserspaceSLIRPStack => DomainNetInterfaceXML { - mac: NetMacAddress { address: n.mac }, + mac: NetMacAddress { + address: n.mac.to_string(), + }, r#type: "user".to_string(), source: None, }, NetworkType::DefinedNetwork { network } => DomainNetInterfaceXML { - mac: NetMacAddress { address: n.mac }, + mac: NetMacAddress { + address: n.mac.to_string(), + }, r#type: "network".to_string(), - source: Some(NetIntSourceXML { network }), + source: Some(NetIntSourceXML { + network: network.to_string(), + }), }, }) } 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 { @@ -487,8 +493,8 @@ pub struct IPV6Config { /// Network configuration #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] pub struct NetworkInfo { - name: String, - uuid: Option, + pub name: String, + pub uuid: Option, title: Option, description: Option, forward_mode: NetworkForwardMode, @@ -501,7 +507,7 @@ pub struct NetworkInfo { } impl NetworkInfo { - pub fn to_virt_network(self) -> anyhow::Result { + pub fn as_virt_network(&self) -> anyhow::Result { if !regex!("^[a-zA-Z0-9]+$").is_match(&self.name) { return Err(StructureExtraction("network name is invalid!").into()); } @@ -532,7 +538,7 @@ impl NetworkInfo { let mut ips = Vec::with_capacity(2); - if let Some(ipv4) = self.ip_v4 { + if let Some(ipv4) = &self.ip_v4 { if ipv4.prefix > 32 { return Err(StructureExtraction("IPv4 prefix is invalid!").into()); } @@ -545,17 +551,17 @@ impl NetworkInfo { .unwrap() .mask() .into(), - dhcp: ipv4.dhcp.map(|dhcp| NetworkDHCPXML { + dhcp: ipv4.dhcp.as_ref().map(|dhcp| NetworkDHCPXML { range: NetworkDHCPRangeXML { start: IpAddr::V4(dhcp.start), end: IpAddr::V4(dhcp.end), }, hosts: dhcp .hosts - .into_iter() + .iter() .map(|c| NetworkDHCPHostXML { - mac: c.mac, - name: c.name, + mac: c.mac.to_string(), + name: c.name.to_string(), ip: c.ip.into(), }) .collect::>(), @@ -563,7 +569,7 @@ impl NetworkInfo { }) } - if let Some(ipv6) = self.ip_v6 { + if let Some(ipv6) = &self.ip_v6 { ips.push(NetworkIPXML { family: "ipv6".to_string(), address: IpAddr::V6(ipv6.bridge_address), @@ -572,17 +578,17 @@ impl NetworkInfo { .unwrap() .mask() .into(), - dhcp: ipv6.dhcp.map(|dhcp| NetworkDHCPXML { + dhcp: ipv6.dhcp.as_ref().map(|dhcp| NetworkDHCPXML { range: NetworkDHCPRangeXML { start: IpAddr::V6(dhcp.start), end: IpAddr::V6(dhcp.end), }, hosts: dhcp .hosts - .into_iter() + .iter() .map(|h| NetworkDHCPHostXML { mac: "".to_string(), - name: h.name, + name: h.name.to_string(), ip: h.ip.into(), }) .collect(), @@ -591,24 +597,24 @@ impl NetworkInfo { } Ok(NetworkXML { - name: self.name, + name: self.name.to_string(), uuid: self.uuid, - title: self.title, - description: self.description, + title: self.title.clone(), + description: self.description.clone(), forward: match self.forward_mode { NetworkForwardMode::NAT => Some(NetworkForwardXML { mode: "nat".to_string(), - dev: self.device.unwrap_or_default(), + dev: self.device.clone().unwrap_or_default(), }), NetworkForwardMode::Isolated => None, }, - bridge: self.bridge_name.map(|b| NetworkBridgeXML { + 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.map(|name| NetworkDomainXML { name }), + domain: self.domain.clone().map(|name| NetworkDomainXML { name }), ips, }) } diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index 00522ff..c728db0 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -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()