From ab16bd7bcf9473836ca729818193f476e362e339 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 17 Jun 2025 21:17:25 +0200 Subject: [PATCH] Can export entire server configuration --- virtweb_backend/Cargo.lock | 200 ++++++++++++++++++ virtweb_backend/Cargo.toml | 2 + virtweb_backend/src/controllers/mod.rs | 7 + .../src/controllers/server_controller.rs | 96 ++++++++- virtweb_backend/src/main.rs | 4 + virtweb_backend/src/utils/time_utils.rs | 13 ++ virtweb_frontend/src/api/ServerApi.ts | 12 ++ virtweb_frontend/src/routes/SysInfoRoute.tsx | 33 ++- .../src/widgets/tokens/TokenRightsEditor.tsx | 5 + 9 files changed, 369 insertions(+), 3 deletions(-) diff --git a/virtweb_backend/Cargo.lock b/virtweb_backend/Cargo.lock index 12df018..3c4c9c5 100644 --- a/virtweb_backend/Cargo.lock +++ b/virtweb_backend/Cargo.lock @@ -435,6 +435,21 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.18" @@ -496,6 +511,9 @@ name = "arbitrary" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "arg_enum_proc_macro" @@ -715,6 +733,25 @@ dependencies = [ "bytes", ] +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "cc" version = "1.2.23" @@ -748,6 +785,20 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "cipher" version = "0.4.4" @@ -816,6 +867,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "convert_case" version = "0.4.0" @@ -981,6 +1038,12 @@ dependencies = [ "syn", ] +[[package]] +name = "deflate64" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" + [[package]] name = "der" version = "0.7.10" @@ -1001,6 +1064,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "derive_more" version = "0.99.20" @@ -1221,6 +1295,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -1380,9 +1455,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -1641,6 +1718,30 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.0.0" @@ -1997,6 +2098,26 @@ dependencies = [ "cc", ] +[[package]] +name = "liblzma" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66352d7a8ac12d4877b6e6ea5a9b7650ee094257dc40889955bea5bc5b08c1d0" +dependencies = [ + "liblzma-sys", +] + +[[package]] +name = "liblzma-sys" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01b9596486f6d60c3bbe644c0e1be1aa6ccc472ad630fe8927b456973d7cb736" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "libyml" version = "0.0.5" @@ -2007,6 +2128,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "libz-rs-sys" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" +dependencies = [ + "zlib-rs", +] + [[package]] name = "light-openid" version = "1.0.4" @@ -2429,6 +2559,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "pem" version = "3.0.5" @@ -3781,6 +3921,7 @@ dependencies = [ "actix-ws", "anyhow", "basic-jwt", + "chrono", "clap", "dotenvy", "env_logger", @@ -3808,6 +3949,7 @@ dependencies = [ "url", "uuid", "virt", + "zip", ] [[package]] @@ -4346,6 +4488,20 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zerotrie" @@ -4380,6 +4536,50 @@ dependencies = [ "syn", ] +[[package]] +name = "zip" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7dcdb4229c0e79c2531a24de7726a0e980417a74fb4d030a35f535665439a0" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "deflate64", + "flate2", + "getrandom 0.3.3", + "hmac", + "indexmap", + "liblzma", + "memchr", + "pbkdf2", + "sha1", + "time", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zlib-rs" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" + +[[package]] +name = "zopfli" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + [[package]] name = "zstd" version = "0.13.3" diff --git a/virtweb_backend/Cargo.toml b/virtweb_backend/Cargo.toml index 641427e..250eeca 100644 --- a/virtweb_backend/Cargo.toml +++ b/virtweb_backend/Cargo.toml @@ -45,3 +45,5 @@ rust-embed = { version = "8.7.2", features = ["mime-guess"] } dotenvy = "0.15.7" nix = { version = "0.30.1", features = ["net"] } basic-jwt = "0.3.0" +zip = "4.1.0" +chrono = "0.4.41" \ No newline at end of file diff --git a/virtweb_backend/src/controllers/mod.rs b/virtweb_backend/src/controllers/mod.rs index 0f8f42d..96c95d3 100644 --- a/virtweb_backend/src/controllers/mod.rs +++ b/virtweb_backend/src/controllers/mod.rs @@ -4,6 +4,7 @@ use actix_web::body::BoxBody; use actix_web::{HttpResponse, web}; use std::error::Error; use std::fmt::{Display, Formatter}; +use zip::result::ZipError; pub mod api_tokens_controller; pub mod auth_controller; @@ -102,6 +103,12 @@ impl From for HttpErr { } } +impl From for HttpErr { + fn from(value: ZipError) -> Self { + HttpErr::Err(std::io::Error::other(value.to_string()).into()) + } +} + impl From for HttpErr { fn from(value: HttpResponse) -> Self { HttpErr::HTTPResponse(value) diff --git a/virtweb_backend/src/controllers/server_controller.rs b/virtweb_backend/src/controllers/server_controller.rs index 0c478e6..909d0b8 100644 --- a/virtweb_backend/src/controllers/server_controller.rs +++ b/virtweb_backend/src/controllers/server_controller.rs @@ -1,14 +1,24 @@ use crate::actors::vnc_tokens_actor::VNC_TOKEN_LIFETIME; use crate::app_config::AppConfig; -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::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 crate::nat::nat_hook; use crate::utils::net_utils; -use actix_web::{HttpResponse, Responder}; +use crate::utils::time_utils::{format_date, time}; +use crate::{api_tokens, constants}; +use actix_files::NamedFile; +use actix_web::{HttpRequest, HttpResponse, Responder}; +use serde::Serialize; +use std::fs::File; +use std::io::Write; use sysinfo::{Components, Disks, Networks, System}; +use zip::ZipWriter; +use zip::write::SimpleFileOptions; #[derive(serde::Serialize)] struct StaticConfig { @@ -199,3 +209,85 @@ pub async fn networks_list() -> HttpResult { pub async fn bridges_list() -> HttpResult { Ok(HttpResponse::Ok().json(net_utils::bridges_list()?)) } + +/// Add JSON file to ZIP +fn zip_json( + zip: &mut ZipWriter, + dir: &str, + content: &Vec, + file_name: F, +) -> anyhow::Result<()> +where + F: Fn(&E) -> String, +{ + for entry in content { + let file_encoded = serde_json::to_string(&entry)?; + + let options = SimpleFileOptions::default() + .compression_method(zip::CompressionMethod::Deflated) + .unix_permissions(0o750); + + zip.start_file(format!("{dir}/{}.json", file_name(entry)), options)?; + zip.write_all(file_encoded.as_bytes())?; + } + Ok(()) +} + +/// Export all configuration elements at once +pub async fn export_all_configs(req: HttpRequest, client: LibVirtReq) -> HttpResult { + // Perform extractions + let vms = client + .get_full_domains_list() + .await? + .into_iter() + .map(VMInfo::from_domain) + .collect::, _>>()?; + let networks = client + .get_full_networks_list() + .await? + .into_iter() + .map(NetworkInfo::from_xml) + .collect::, _>>()?; + let nw_filters = client + .get_full_network_filters_list() + .await? + .into_iter() + .map(NetworkFilter::lib2rest) + .collect::, _>>()?; + let tokens = api_tokens::full_list().await?; + + // Create ZIP file + let dest_dir = tempfile::tempdir_in(&AppConfig::get().temp_dir)?; + let zip_path = dest_dir.path().join("export.zip"); + + let file = File::create(&zip_path)?; + let mut zip = ZipWriter::new(file); + + // Encode entities to JSON + zip_json(&mut zip, "vms", &vms, |v| v.name.to_string())?; + zip_json(&mut zip, "networks", &networks, |v| v.name.0.to_string())?; + zip_json( + &mut zip, + "nw_filters", + &nw_filters, + |v| match constants::BUILTIN_NETWORK_FILTER_RULES.contains(&v.name.0.as_str()) { + true => format!("builtin/{}", v.name.0), + false => v.name.0.to_string(), + }, + )?; + zip_json(&mut zip, "tokens", &tokens, |v| v.id.0.to_string())?; + + // Finalize ZIP and return response + zip.finish()?; + let file = File::open(zip_path)?; + + let file = NamedFile::from_file( + file, + format!( + "export_{}.zip", + format_date(time() as i64).unwrap().replace('/', "-") + ), + )?; + + Ok(file.into_response(&req)) +} diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index 8090766..9228b22 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -157,6 +157,10 @@ async fn main() -> std::io::Result<()> { "/api/server/bridges", web::get().to(server_controller::bridges_list), ) + .route( + "/api/server/export_configs", + web::get().to(server_controller::export_all_configs), + ) // Auth controller .route( "/api/auth/local", diff --git a/virtweb_backend/src/utils/time_utils.rs b/virtweb_backend/src/utils/time_utils.rs index 5ff9a92..5176443 100644 --- a/virtweb_backend/src/utils/time_utils.rs +++ b/virtweb_backend/src/utils/time_utils.rs @@ -1,3 +1,4 @@ +use chrono::Datelike; use std::time::{SystemTime, UNIX_EPOCH}; /// Get the current time since epoch @@ -13,3 +14,15 @@ pub fn time() -> u64 { .unwrap() .as_secs() } + +/// Format given UNIX time in a simple format +pub fn format_date(time: i64) -> anyhow::Result { + let date = chrono::DateTime::from_timestamp(time, 0).ok_or(anyhow::anyhow!("invalid date"))?; + + Ok(format!( + "{:0>2}/{:0>2}/{}", + date.day(), + date.month(), + date.year() + )) +} diff --git a/virtweb_frontend/src/api/ServerApi.ts b/virtweb_frontend/src/api/ServerApi.ts index 563eaeb..3f820f1 100644 --- a/virtweb_frontend/src/api/ServerApi.ts +++ b/virtweb_frontend/src/api/ServerApi.ts @@ -232,4 +232,16 @@ export class ServerApi { }) ).data; } + + /** + * Export all server configs + */ + static async ExportServerConfigs(): Promise { + return ( + await APIClient.exec({ + method: "GET", + uri: "/server/export_configs", + }) + ).data; + } } diff --git a/virtweb_frontend/src/routes/SysInfoRoute.tsx b/virtweb_frontend/src/routes/SysInfoRoute.tsx index dd4c1eb..548bfa3 100644 --- a/virtweb_frontend/src/routes/SysInfoRoute.tsx +++ b/virtweb_frontend/src/routes/SysInfoRoute.tsx @@ -9,18 +9,21 @@ import { import Icon from "@mdi/react"; import { Box, + IconButton, LinearProgress, Table, TableBody, TableCell, TableHead, TableRow, + Tooltip, Typography, } from "@mui/material"; import Grid from "@mui/material/Grid"; import { PieChart } from "@mui/x-charts"; import { filesize } from "filesize"; import humanizeDuration from "humanize-duration"; +import IosShareIcon from "@mui/icons-material/IosShare"; import React from "react"; import { DiskInfo, @@ -31,6 +34,8 @@ import { import { AsyncWidget } from "../widgets/AsyncWidget"; import { VirtWebPaper } from "../widgets/VirtWebPaper"; import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; +import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider"; +import { useAlert } from "../hooks/providers/AlertDialogProvider"; export function SysInfoRoute(): React.ReactElement { const [info, setInfo] = React.useState(); @@ -52,6 +57,23 @@ export function SysInfoRoute(): React.ReactElement { export function SysInfoRouteInner(p: { info: ServerSystemInfo; }): React.ReactElement { + const alert = useAlert(); + const loadingMessage = useLoadingMessage(); + const downloadAllConfig = async () => { + try { + loadingMessage.show("Downloading server config..."); + const res = await ServerApi.ExportServerConfigs(); + + const url = URL.createObjectURL(res); + window.location.href = url; + } catch (e) { + console.error("Failed to download server config!", e); + alert(`Failed to download server config! ${e}`); + } finally { + loadingMessage.hide(); + } + }; + const sumDiskUsage = p.info.disks.reduce( (prev, disk) => { return { @@ -63,7 +85,16 @@ export function SysInfoRouteInner(p: { ); return ( - + + + + + + } + > {/* Memory */} diff --git a/virtweb_frontend/src/widgets/tokens/TokenRightsEditor.tsx b/virtweb_frontend/src/widgets/tokens/TokenRightsEditor.tsx index 33ef9f7..18d4733 100644 --- a/virtweb_frontend/src/widgets/tokens/TokenRightsEditor.tsx +++ b/virtweb_frontend/src/widgets/tokens/TokenRightsEditor.tsx @@ -799,6 +799,11 @@ export function TokenRightsEditor(p: { right={{ verb: "GET", path: "/api/server/bridges" }} label="Get list of network bridges" /> + );