12 Commits

Author SHA1 Message Date
b633694f74 Add api client to release
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-06-18 19:07:43 +02:00
ab16bd7bcf Can export entire server configuration
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-17 21:17:25 +02:00
1080ab5cb2 Fix boolean config
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-17 19:13:43 +02:00
a2845ddafe Enable word wrapping in Monaco editors
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-16 21:53:43 +02:00
c968b64b51 Fix ESLint issues
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-16 21:52:00 +02:00
12833dc6da Fix cloud init command
Some checks failed
continuous-integration/drone/push Build is failing
2025-06-16 21:48:06 +02:00
8c4f2a9f2d Fix issue with Ubuntu cloud images
Some checks failed
continuous-integration/drone/push Build is failing
2025-06-16 21:31:33 +02:00
9a6b6cfb2d Can change default username
Some checks failed
continuous-integration/drone/push Build is failing
2025-06-16 19:56:04 +02:00
b28ca5f27d Add new options
Some checks failed
continuous-integration/drone/push Build is failing
2025-06-16 19:42:57 +02:00
92f187bf91 Start to auto-fill cloudinit fields
Some checks failed
continuous-integration/drone/push Build is failing
2025-06-16 19:31:13 +02:00
9f1f4b44ca Make network configuration editable 2025-06-16 18:51:33 +02:00
b00d46105b Update Rust crate reqwest to 0.12.20
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-15 00:24:45 +00:00
15 changed files with 636 additions and 34 deletions

View File

@ -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

View File

@ -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]]
@ -1622,18 +1699,47 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710"
dependencies = [
"base64 0.22.1",
"bytes",
"futures-channel",
"futures-util",
"http 1.3.1",
"http-body",
"hyper",
"ipnet",
"libc",
"percent-encoding",
"pin-project-lite",
"socket2",
"system-configuration",
"tokio",
"tower-service",
"tracing",
"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]]
@ -1839,6 +1945,16 @@ dependencies = [
"serde",
]
[[package]]
name = "iri-string"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
@ -1982,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"
@ -1992,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"
@ -2414,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"
@ -2783,9 +2938,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "reqwest"
version = "0.12.15"
version = "0.12.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813"
dependencies = [
"base64 0.22.1",
"bytes",
@ -2800,31 +2955,28 @@ dependencies = [
"hyper-rustls",
"hyper-tls",
"hyper-util",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls-pemfile",
"rustls-pki-types",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"system-configuration",
"tokio",
"tokio-native-tls",
"tokio-util",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
"windows-registry",
]
[[package]]
@ -2933,15 +3085,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.12.0"
@ -3278,9 +3421,9 @@ dependencies = [
[[package]]
name = "sysinfo"
version = "0.35.2"
version = "0.35.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3ffa3e4ff2b324a57f7aeb3c349656c7b127c3c189520251a648102a92496e"
checksum = "79251336d17c72d9762b8b54be4befe38d2db56fbbc0241396d70f173c39d47a"
dependencies = [
"libc",
"memchr",
@ -3547,6 +3690,24 @@ dependencies = [
"tower-service",
]
[[package]]
name = "tower-http"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
dependencies = [
"bitflags 2.9.1",
"bytes",
"futures-util",
"http 1.3.1",
"http-body",
"iri-string",
"pin-project-lite",
"tower",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
@ -3760,6 +3921,7 @@ dependencies = [
"actix-ws",
"anyhow",
"basic-jwt",
"chrono",
"clap",
"dotenvy",
"env_logger",
@ -3787,6 +3949,7 @@ dependencies = [
"url",
"uuid",
"virt",
"zip",
]
[[package]]
@ -4325,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"
@ -4359,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"

View File

@ -28,10 +28,10 @@ futures-util = "0.3.31"
anyhow = "1.0.98"
actix-multipart = "0.7.2"
tempfile = "3.20.0"
reqwest = { version = "0.12.15", features = ["stream"] }
reqwest = { version = "0.12.20", features = ["stream"] }
url = "2.5.4"
virt = "0.4.2"
sysinfo = { version = "0.35.2", features = ["serde"] }
sysinfo = { version = "0.35.1", features = ["serde"] }
uuid = { version = "1.16.0", features = ["v4", "serde"] }
lazy-regex = "3.4.1"
thiserror = "2.0.12"
@ -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"

View File

@ -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)

View File

@ -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))
}

View File

@ -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",

View File

@ -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()
))
}

View File

@ -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",

View File

@ -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",

View File

@ -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;
}
}

View File

@ -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 }}>

View File

@ -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}

View File

@ -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?.();
}}
/>
);
}

View File

@ -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>
)}

View File

@ -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>
</>
);