Compare commits
	
		
			11 Commits
		
	
	
		
			a309baa841
			...
			20250618
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| b633694f74 | |||
| ab16bd7bcf | |||
| 1080ab5cb2 | |||
| a2845ddafe | |||
| c968b64b51 | |||
| 12833dc6da | |||
| 8c4f2a9f2d | |||
| 9a6b6cfb2d | |||
| b28ca5f27d | |||
| 92f187bf91 | |||
| 9f1f4b44ca | 
@@ -46,8 +46,9 @@ steps:
 | 
			
		||||
  - cd virtweb_backend
 | 
			
		||||
  - mv /tmp/web_build/dist static
 | 
			
		||||
  - cargo build --release
 | 
			
		||||
  - ls -lah target/release/virtweb_backend
 | 
			
		||||
  - cp target/release/virtweb_backend /tmp/release
 | 
			
		||||
  - cargo build --release --example api_curl
 | 
			
		||||
  - ls -lah target/release/virtweb_backend target/release/examples/api_curl
 | 
			
		||||
  - cp target/release/virtweb_backend target/release/examples/api_curl /tmp/release
 | 
			
		||||
 | 
			
		||||
- name: gitea_release
 | 
			
		||||
  image: plugins/gitea-release
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										200
									
								
								virtweb_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										200
									
								
								virtweb_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -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"
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
@@ -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<actix_web::Error> for HttpErr {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<ZipError> for HttpErr {
 | 
			
		||||
    fn from(value: ZipError) -> Self {
 | 
			
		||||
        HttpErr::Err(std::io::Error::other(value.to_string()).into())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<HttpResponse> for HttpErr {
 | 
			
		||||
    fn from(value: HttpResponse) -> Self {
 | 
			
		||||
        HttpErr::HTTPResponse(value)
 | 
			
		||||
 
 | 
			
		||||
@@ -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<E: Serialize, F>(
 | 
			
		||||
    zip: &mut ZipWriter<File>,
 | 
			
		||||
    dir: &str,
 | 
			
		||||
    content: &Vec<E>,
 | 
			
		||||
    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::<Result<Vec<_>, _>>()?;
 | 
			
		||||
    let networks = client
 | 
			
		||||
        .get_full_networks_list()
 | 
			
		||||
        .await?
 | 
			
		||||
        .into_iter()
 | 
			
		||||
        .map(NetworkInfo::from_xml)
 | 
			
		||||
        .collect::<Result<Vec<_>, _>>()?;
 | 
			
		||||
    let nw_filters = client
 | 
			
		||||
        .get_full_network_filters_list()
 | 
			
		||||
        .await?
 | 
			
		||||
        .into_iter()
 | 
			
		||||
        .map(NetworkFilter::lib2rest)
 | 
			
		||||
        .collect::<Result<Vec<_>, _>>()?;
 | 
			
		||||
    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))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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",
 | 
			
		||||
 
 | 
			
		||||
@@ -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<String> {
 | 
			
		||||
    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()
 | 
			
		||||
    ))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										55
									
								
								virtweb_frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										55
									
								
								virtweb_frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -16,7 +16,7 @@
 | 
			
		||||
        "@monaco-editor/react": "^4.7.0",
 | 
			
		||||
        "@mui/icons-material": "^7.1.1",
 | 
			
		||||
        "@mui/material": "^7.1.1",
 | 
			
		||||
        "@mui/x-charts": "^8.5.2",
 | 
			
		||||
        "@mui/x-charts": "^8.3.1",
 | 
			
		||||
        "@mui/x-data-grid": "^8.3.1",
 | 
			
		||||
        "date-and-time": "^3.6.0",
 | 
			
		||||
        "filesize": "^10.1.6",
 | 
			
		||||
@@ -29,7 +29,8 @@
 | 
			
		||||
        "react-syntax-highlighter": "^15.6.1",
 | 
			
		||||
        "react-vnc": "^3.1.0",
 | 
			
		||||
        "uuid": "^11.1.0",
 | 
			
		||||
        "xml-formatter": "^3.6.6"
 | 
			
		||||
        "xml-formatter": "^3.6.6",
 | 
			
		||||
        "yaml": "^2.8.0"
 | 
			
		||||
      },
 | 
			
		||||
      "devDependencies": {
 | 
			
		||||
        "@eslint/js": "^9.27.0",
 | 
			
		||||
@@ -291,9 +292,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@babel/runtime": {
 | 
			
		||||
      "version": "7.27.6",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
 | 
			
		||||
      "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
 | 
			
		||||
      "version": "7.27.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz",
 | 
			
		||||
      "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=6.9.0"
 | 
			
		||||
@@ -1218,15 +1219,15 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@mui/x-charts": {
 | 
			
		||||
      "version": "8.5.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-8.5.2.tgz",
 | 
			
		||||
      "integrity": "sha512-JLPTtd9m8CWMoIxwHFM9QpPDpfdsetfkCErJUvsyQnj/rC8sBMmQqk0c1olusA+OqTyVT3gGmiqXXFar/0cvkw==",
 | 
			
		||||
      "version": "8.3.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-8.3.1.tgz",
 | 
			
		||||
      "integrity": "sha512-jZClK40++ftcMwCeHKudGKmazd0MsgnrIP6RhYi2lH1kg0jK2upueokyxVIIxqquwWsQYE3WsflJBP61DvYXOQ==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@babel/runtime": "^7.27.6",
 | 
			
		||||
        "@mui/utils": "^7.1.1",
 | 
			
		||||
        "@mui/x-charts-vendor": "8.5.2",
 | 
			
		||||
        "@mui/x-internals": "8.5.2",
 | 
			
		||||
        "@babel/runtime": "^7.27.1",
 | 
			
		||||
        "@mui/utils": "^7.0.2",
 | 
			
		||||
        "@mui/x-charts-vendor": "8.3.1",
 | 
			
		||||
        "@mui/x-internals": "8.3.1",
 | 
			
		||||
        "bezier-easing": "^2.1.0",
 | 
			
		||||
        "clsx": "^2.1.1",
 | 
			
		||||
        "prop-types": "^15.8.1",
 | 
			
		||||
@@ -1254,12 +1255,12 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@mui/x-charts-vendor": {
 | 
			
		||||
      "version": "8.5.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-8.5.2.tgz",
 | 
			
		||||
      "integrity": "sha512-93KFrEpo3Xhr0g2TQsbtPVqGAsbkKBN5J57ykrCM5GxFmq3kDGFU4k9+FpKiaIYYL8ijzgHGNh+jNVbP0pq3rQ==",
 | 
			
		||||
      "version": "8.3.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-8.3.1.tgz",
 | 
			
		||||
      "integrity": "sha512-UcUa7HDIpSfeVBYgeHewWoVALcB4Gg9we53l78j2cyadYBZOWdtLj8fezo9zAhxfZ5s9T+1yIyuD+CCnYJnUpQ==",
 | 
			
		||||
      "license": "MIT AND ISC",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@babel/runtime": "^7.27.6",
 | 
			
		||||
        "@babel/runtime": "^7.27.1",
 | 
			
		||||
        "@types/d3-color": "^3.1.3",
 | 
			
		||||
        "@types/d3-delaunay": "^6.0.4",
 | 
			
		||||
        "@types/d3-interpolate": "^3.0.4",
 | 
			
		||||
@@ -1278,28 +1279,6 @@
 | 
			
		||||
        "robust-predicates": "^3.0.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@mui/x-charts/node_modules/@mui/x-internals": {
 | 
			
		||||
      "version": "8.5.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.5.2.tgz",
 | 
			
		||||
      "integrity": "sha512-5YhB2AekK7G8d0YrAjg3WNf0uy3V73JD98WNxJhbIlCraQgl8QOQzr2zNO7MAf/X7mZQtjpjuAsiG3+gI2NVyg==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@babel/runtime": "^7.27.6",
 | 
			
		||||
        "@mui/utils": "^7.1.1",
 | 
			
		||||
        "reselect": "^5.1.1"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=14.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "type": "opencollective",
 | 
			
		||||
        "url": "https://opencollective.com/mui-org"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
 | 
			
		||||
        "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@mui/x-data-grid": {
 | 
			
		||||
      "version": "8.3.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-8.3.1.tgz",
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@
 | 
			
		||||
    "@monaco-editor/react": "^4.7.0",
 | 
			
		||||
    "@mui/icons-material": "^7.1.1",
 | 
			
		||||
    "@mui/material": "^7.1.1",
 | 
			
		||||
    "@mui/x-charts": "^8.5.2",
 | 
			
		||||
    "@mui/x-charts": "^8.3.1",
 | 
			
		||||
    "@mui/x-data-grid": "^8.3.1",
 | 
			
		||||
    "date-and-time": "^3.6.0",
 | 
			
		||||
    "filesize": "^10.1.6",
 | 
			
		||||
@@ -31,7 +31,8 @@
 | 
			
		||||
    "react-syntax-highlighter": "^15.6.1",
 | 
			
		||||
    "react-vnc": "^3.1.0",
 | 
			
		||||
    "uuid": "^11.1.0",
 | 
			
		||||
    "xml-formatter": "^3.6.6"
 | 
			
		||||
    "xml-formatter": "^3.6.6",
 | 
			
		||||
    "yaml": "^2.8.0"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@eslint/js": "^9.27.0",
 | 
			
		||||
 
 | 
			
		||||
@@ -232,4 +232,16 @@ export class ServerApi {
 | 
			
		||||
      })
 | 
			
		||||
    ).data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Export all server configs
 | 
			
		||||
   */
 | 
			
		||||
  static async ExportServerConfigs(): Promise<Blob> {
 | 
			
		||||
    return (
 | 
			
		||||
      await APIClient.exec({
 | 
			
		||||
        method: "GET",
 | 
			
		||||
        uri: "/server/export_configs",
 | 
			
		||||
      })
 | 
			
		||||
    ).data;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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<ServerSystemInfo>();
 | 
			
		||||
@@ -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 (
 | 
			
		||||
    <VirtWebRouteContainer label="Sysinfo">
 | 
			
		||||
    <VirtWebRouteContainer
 | 
			
		||||
      label="Sysinfo"
 | 
			
		||||
      actions={
 | 
			
		||||
        <Tooltip title="Export all server configs">
 | 
			
		||||
          <IconButton onClick={downloadAllConfig}>
 | 
			
		||||
            <IosShareIcon />
 | 
			
		||||
          </IconButton>
 | 
			
		||||
        </Tooltip>
 | 
			
		||||
      }
 | 
			
		||||
    >
 | 
			
		||||
      <Grid container spacing={2}>
 | 
			
		||||
        {/* Memory */}
 | 
			
		||||
        <Grid size={{ xs: 4 }}>
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,9 @@ export function CheckboxInput(p: {
 | 
			
		||||
        <Checkbox
 | 
			
		||||
          disabled={!p.editable}
 | 
			
		||||
          checked={p.checked}
 | 
			
		||||
          onChange={(e) => { p.onValueChange(e.target.checked); }}
 | 
			
		||||
          onChange={(e) => {
 | 
			
		||||
            p.onValueChange(e.target.checked);
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      }
 | 
			
		||||
      label={p.label}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,14 @@
 | 
			
		||||
/* eslint-disable @typescript-eslint/no-base-to-string */
 | 
			
		||||
 | 
			
		||||
import Editor from "@monaco-editor/react";
 | 
			
		||||
import BookIcon from "@mui/icons-material/Book";
 | 
			
		||||
import RefreshIcon from "@mui/icons-material/Refresh";
 | 
			
		||||
import { Grid, IconButton, InputAdornment, Tooltip } from "@mui/material";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { v4 as uuidv4 } from "uuid";
 | 
			
		||||
import YAML from "yaml";
 | 
			
		||||
import { VMInfo } from "../../api/VMApi";
 | 
			
		||||
import { RouterLink } from "../RouterLink";
 | 
			
		||||
import { CheckboxInput } from "./CheckboxInput";
 | 
			
		||||
import { EditSection } from "./EditSection";
 | 
			
		||||
import { SelectInput } from "./SelectInput";
 | 
			
		||||
@@ -38,6 +44,14 @@ export function CloudInitEditor(p: CloudInitProps): React.ReactElement {
 | 
			
		||||
          {...p}
 | 
			
		||||
          editable={p.editable && p.vm.cloud_init.attach_config}
 | 
			
		||||
        />
 | 
			
		||||
        <CloudInitNetworkConfig
 | 
			
		||||
          {...p}
 | 
			
		||||
          editable={p.editable && p.vm.cloud_init.attach_config}
 | 
			
		||||
        />
 | 
			
		||||
        <CloudInitUserDataAssistant
 | 
			
		||||
          {...p}
 | 
			
		||||
          editable={p.editable && p.vm.cloud_init.attach_config}
 | 
			
		||||
        />
 | 
			
		||||
      </Grid>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
@@ -108,12 +122,27 @@ function CloudInitMetadata(p: CloudInitProps): React.ReactElement {
 | 
			
		||||
 | 
			
		||||
function CloudInitRawUserData(p: CloudInitProps): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <EditSection title="User data">
 | 
			
		||||
    <EditSection
 | 
			
		||||
      title="User data"
 | 
			
		||||
      actions={
 | 
			
		||||
        <RouterLink
 | 
			
		||||
          target="_blank"
 | 
			
		||||
          to="https://cloudinit.readthedocs.io/en/latest/reference/index.html"
 | 
			
		||||
        >
 | 
			
		||||
          <Tooltip title="Official reference">
 | 
			
		||||
            <IconButton size="small">
 | 
			
		||||
              <BookIcon />
 | 
			
		||||
            </IconButton>
 | 
			
		||||
          </Tooltip>
 | 
			
		||||
        </RouterLink>
 | 
			
		||||
      }
 | 
			
		||||
    >
 | 
			
		||||
      <Editor
 | 
			
		||||
        theme="vs-dark"
 | 
			
		||||
        options={{
 | 
			
		||||
          readOnly: !p.editable,
 | 
			
		||||
          quickSuggestions: { other: true, comments: true, strings: true },
 | 
			
		||||
          wordWrap: "on",
 | 
			
		||||
        }}
 | 
			
		||||
        language="yaml"
 | 
			
		||||
        height={"30vh"}
 | 
			
		||||
@@ -126,3 +155,187 @@ function CloudInitRawUserData(p: CloudInitProps): React.ReactElement {
 | 
			
		||||
    </EditSection>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function CloudInitNetworkConfig(p: CloudInitProps): React.ReactElement {
 | 
			
		||||
  if (!p.editable && !p.vm.cloud_init.network_configuration) return <></>;
 | 
			
		||||
  return (
 | 
			
		||||
    <EditSection
 | 
			
		||||
      title="Network configuration"
 | 
			
		||||
      actions={
 | 
			
		||||
        <RouterLink
 | 
			
		||||
          target="_blank"
 | 
			
		||||
          to="https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v2.html"
 | 
			
		||||
        >
 | 
			
		||||
          <Tooltip title="Official network configuration reference">
 | 
			
		||||
            <IconButton size="small">
 | 
			
		||||
              <BookIcon />
 | 
			
		||||
            </IconButton>
 | 
			
		||||
          </Tooltip>
 | 
			
		||||
        </RouterLink>
 | 
			
		||||
      }
 | 
			
		||||
    >
 | 
			
		||||
      <Editor
 | 
			
		||||
        theme="vs-dark"
 | 
			
		||||
        options={{
 | 
			
		||||
          readOnly: !p.editable,
 | 
			
		||||
          quickSuggestions: { other: true, comments: true, strings: true },
 | 
			
		||||
          wordWrap: "on",
 | 
			
		||||
        }}
 | 
			
		||||
        language="yaml"
 | 
			
		||||
        height={"30vh"}
 | 
			
		||||
        value={p.vm.cloud_init.network_configuration ?? ""}
 | 
			
		||||
        onChange={(v) => {
 | 
			
		||||
          if (v && v !== "") p.vm.cloud_init.network_configuration = v;
 | 
			
		||||
          else p.vm.cloud_init.network_configuration = undefined;
 | 
			
		||||
          p.onChange?.();
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
    </EditSection>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function CloudInitUserDataAssistant(p: CloudInitProps): React.ReactElement {
 | 
			
		||||
  const user_data = React.useMemo(() => {
 | 
			
		||||
    return YAML.parseDocument(p.vm.cloud_init.user_data);
 | 
			
		||||
  }, [p.vm.cloud_init.user_data]);
 | 
			
		||||
 | 
			
		||||
  const onChange = () => {
 | 
			
		||||
    p.vm.cloud_init.user_data = user_data.toString();
 | 
			
		||||
 | 
			
		||||
    if (!p.vm.cloud_init.user_data.startsWith("#cloud-config"))
 | 
			
		||||
      p.vm.cloud_init.user_data = `#cloud-config\n${p.vm.cloud_init.user_data}`;
 | 
			
		||||
 | 
			
		||||
    p.onChange?.();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const SYSTEMD_NOT_SERIAL = `/bin/sh -c "rm -f /etc/default/grub.d/50-cloudimg-settings.cfg && sed -i 's/quiet splash//g' /etc/default/grub && update-grub"`;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <EditSection title="User data assistant">
 | 
			
		||||
      <CloudInitTextInput
 | 
			
		||||
        editable={p.editable}
 | 
			
		||||
        name="Default user name"
 | 
			
		||||
        refUrl="https://cloudinit.readthedocs.io/en/latest/reference/modules.html#set-passwords"
 | 
			
		||||
        attrPath={["user", "name"]}
 | 
			
		||||
        onChange={onChange}
 | 
			
		||||
        yaml={user_data}
 | 
			
		||||
      />
 | 
			
		||||
      <CloudInitTextInput
 | 
			
		||||
        editable={p.editable}
 | 
			
		||||
        name="Default user password"
 | 
			
		||||
        refUrl="https://cloudinit.readthedocs.io/en/latest/reference/modules.html#set-passwords"
 | 
			
		||||
        attrPath={["password"]}
 | 
			
		||||
        onChange={onChange}
 | 
			
		||||
        yaml={user_data}
 | 
			
		||||
      />
 | 
			
		||||
      <CloudInitBooleanInput
 | 
			
		||||
        editable={p.editable}
 | 
			
		||||
        name="Expire password to require new password on next login"
 | 
			
		||||
        yaml={user_data}
 | 
			
		||||
        attrPath={["chpasswd", "expire"]}
 | 
			
		||||
        onChange={onChange}
 | 
			
		||||
        refUrl="https://cloudinit.readthedocs.io/en/latest/reference/modules.html#set-passwords"
 | 
			
		||||
      />
 | 
			
		||||
      <br />
 | 
			
		||||
      <CloudInitBooleanInput
 | 
			
		||||
        editable={p.editable}
 | 
			
		||||
        name="Enable SSH password auth"
 | 
			
		||||
        yaml={user_data}
 | 
			
		||||
        attrPath={["ssh_pwauth"]}
 | 
			
		||||
        onChange={onChange}
 | 
			
		||||
        refUrl="https://cloudinit.readthedocs.io/en/latest/reference/modules.html#set-passwords"
 | 
			
		||||
      />
 | 
			
		||||
      <CloudInitTextInput
 | 
			
		||||
        editable={p.editable}
 | 
			
		||||
        name="Keyboard layout"
 | 
			
		||||
        refUrl="https://cloudinit.readthedocs.io/en/latest/reference/modules.html#keyboard"
 | 
			
		||||
        attrPath={["keyboard", "layout"]}
 | 
			
		||||
        onChange={onChange}
 | 
			
		||||
        yaml={user_data}
 | 
			
		||||
      />
 | 
			
		||||
      <CloudInitTextInput
 | 
			
		||||
        editable={p.editable}
 | 
			
		||||
        name="Final message"
 | 
			
		||||
        refUrl="https://cloudinit.readthedocs.io/en/latest/reference/modules.html#final-message"
 | 
			
		||||
        attrPath={["final_message"]}
 | 
			
		||||
        onChange={onChange}
 | 
			
		||||
        yaml={user_data}
 | 
			
		||||
      />
 | 
			
		||||
      {/* /bin/sh -c "rm -f /etc/default/grub.d/50-cloudimg-settings.cfg && update-grub" */}
 | 
			
		||||
      <CheckboxInput
 | 
			
		||||
        editable={p.editable}
 | 
			
		||||
        label="Show all startup messages on tty1, not serial"
 | 
			
		||||
        checked={
 | 
			
		||||
          !!(user_data.get("runcmd") as any)?.items.find(
 | 
			
		||||
            (a: any) => a.value === SYSTEMD_NOT_SERIAL
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
        onValueChange={(c) => {
 | 
			
		||||
          if (!user_data.getIn(["runcmd"])) user_data.addIn(["runcmd"], []);
 | 
			
		||||
 | 
			
		||||
          const runcmd = user_data.getIn(["runcmd"]) as any;
 | 
			
		||||
 | 
			
		||||
          if (c) {
 | 
			
		||||
            runcmd.addIn([], SYSTEMD_NOT_SERIAL);
 | 
			
		||||
          } else {
 | 
			
		||||
            const idx = runcmd.items.findIndex(
 | 
			
		||||
              (o: any) => o.value === SYSTEMD_NOT_SERIAL
 | 
			
		||||
            );
 | 
			
		||||
            runcmd.items.splice(idx, 1);
 | 
			
		||||
          }
 | 
			
		||||
          onChange();
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
    </EditSection>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function CloudInitTextInput(p: {
 | 
			
		||||
  editable: boolean;
 | 
			
		||||
  name: string;
 | 
			
		||||
  refUrl: string;
 | 
			
		||||
  attrPath: Iterable<unknown>;
 | 
			
		||||
  yaml: YAML.Document;
 | 
			
		||||
  onChange?: () => void;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <TextInput
 | 
			
		||||
      editable={p.editable}
 | 
			
		||||
      label={p.name}
 | 
			
		||||
      value={String(p.yaml.getIn(p.attrPath) ?? "")}
 | 
			
		||||
      onValueChange={(v) => {
 | 
			
		||||
        if (v !== undefined) p.yaml.setIn(p.attrPath, v);
 | 
			
		||||
        else p.yaml.deleteIn(p.attrPath);
 | 
			
		||||
        p.onChange?.();
 | 
			
		||||
      }}
 | 
			
		||||
      endAdornment={
 | 
			
		||||
        <RouterLink to={p.refUrl} target="_blank">
 | 
			
		||||
          <IconButton size="small">
 | 
			
		||||
            <BookIcon />
 | 
			
		||||
          </IconButton>
 | 
			
		||||
        </RouterLink>
 | 
			
		||||
      }
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function CloudInitBooleanInput(p: {
 | 
			
		||||
  editable: boolean;
 | 
			
		||||
  name: string;
 | 
			
		||||
  refUrl: string;
 | 
			
		||||
  attrPath: Iterable<unknown>;
 | 
			
		||||
  yaml: YAML.Document;
 | 
			
		||||
  onChange?: () => void;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <CheckboxInput
 | 
			
		||||
      editable={p.editable}
 | 
			
		||||
      label={p.name}
 | 
			
		||||
      checked={p.yaml.getIn(p.attrPath) === true}
 | 
			
		||||
      onValueChange={(v) => {
 | 
			
		||||
        p.yaml.setIn(p.attrPath, v);
 | 
			
		||||
        p.onChange?.();
 | 
			
		||||
      }}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -19,13 +19,10 @@ export function EditSection(
 | 
			
		||||
              display: "flex",
 | 
			
		||||
              justifyContent: "space-between",
 | 
			
		||||
              alignItems: "center",
 | 
			
		||||
              marginBottom: "15px",
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            {p.title && (
 | 
			
		||||
              <Typography variant="h5" style={{ marginBottom: "15px" }}>
 | 
			
		||||
                {p.title}
 | 
			
		||||
              </Typography>
 | 
			
		||||
            )}
 | 
			
		||||
            {p.title && <Typography variant="h5">{p.title}</Typography>}
 | 
			
		||||
            {p.actions}
 | 
			
		||||
          </span>
 | 
			
		||||
        )}
 | 
			
		||||
 
 | 
			
		||||
@@ -799,6 +799,11 @@ export function TokenRightsEditor(p: {
 | 
			
		||||
          right={{ verb: "GET", path: "/api/server/bridges" }}
 | 
			
		||||
          label="Get list of network bridges"
 | 
			
		||||
        />
 | 
			
		||||
        <RouteRight
 | 
			
		||||
          {...p}
 | 
			
		||||
          right={{ verb: "GET", path: "/api/server/export_configs" }}
 | 
			
		||||
          label="Export all configurations"
 | 
			
		||||
        />
 | 
			
		||||
      </RightsSection>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user