1 Commits

Author SHA1 Message Date
bae22e9daf Update dependency eslint-plugin-react-hooks to ^5.2.0
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-05-31 00:09:43 +00:00
46 changed files with 344 additions and 2087 deletions

View File

@ -46,9 +46,8 @@ steps:
- cd virtweb_backend - cd virtweb_backend
- mv /tmp/web_build/dist static - mv /tmp/web_build/dist static
- cargo build --release - cargo build --release
- cargo build --release --example api_curl - ls -lah target/release/virtweb_backend
- ls -lah target/release/virtweb_backend target/release/examples/api_curl - cp target/release/virtweb_backend /tmp/release
- cp target/release/virtweb_backend target/release/examples/api_curl /tmp/release
- name: gitea_release - name: gitea_release
image: plugins/gitea-release image: plugins/gitea-release

View File

@ -435,21 +435,6 @@ dependencies = [
"alloc-no-stdlib", "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]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.18" version = "0.6.18"
@ -511,9 +496,6 @@ name = "arbitrary"
version = "1.4.1" version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
dependencies = [
"derive_arbitrary",
]
[[package]] [[package]]
name = "arg_enum_proc_macro" name = "arg_enum_proc_macro"
@ -733,25 +715,6 @@ dependencies = [
"bytes", "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]] [[package]]
name = "cc" name = "cc"
version = "1.2.23" version = "1.2.23"
@ -785,20 +748,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 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]] [[package]]
name = "cipher" name = "cipher"
version = "0.4.4" version = "0.4.4"
@ -811,9 +760,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.40" version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -821,9 +770,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.40" version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -833,9 +782,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.40" version = "4.5.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@ -867,12 +816,6 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "constant_time_eq"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]] [[package]]
name = "convert_case" name = "convert_case"
version = "0.4.0" version = "0.4.0"
@ -1038,12 +981,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "deflate64"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b"
[[package]] [[package]]
name = "der" name = "der"
version = "0.7.10" version = "0.7.10"
@ -1064,17 +1001,6 @@ dependencies = [
"powerfmt", "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]] [[package]]
name = "derive_more" name = "derive_more"
version = "0.99.20" version = "0.99.20"
@ -1295,7 +1221,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"libz-rs-sys",
"miniz_oxide", "miniz_oxide",
] ]
@ -1455,11 +1380,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"r-efi", "r-efi",
"wasi 0.14.2+wasi-0.2.4", "wasi 0.14.2+wasi-0.2.4",
"wasm-bindgen",
] ]
[[package]] [[package]]
@ -1699,47 +1622,18 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710" checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710"
dependencies = [ dependencies = [
"base64 0.22.1",
"bytes", "bytes",
"futures-channel", "futures-channel",
"futures-util", "futures-util",
"http 1.3.1", "http 1.3.1",
"http-body", "http-body",
"hyper", "hyper",
"ipnet",
"libc", "libc",
"percent-encoding",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2",
"system-configuration",
"tokio", "tokio",
"tower-service", "tower-service",
"tracing", "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]] [[package]]
@ -1945,16 +1839,6 @@ dependencies = [
"serde", "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]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
version = "1.70.1" version = "1.70.1"
@ -2098,45 +1982,6 @@ dependencies = [
"cc", "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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3302702afa434ffa30847a83305f0a69d6abd74293b6554c18ec85c7ef30c980"
dependencies = [
"anyhow",
"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]] [[package]]
name = "light-openid" name = "light-openid"
version = "1.0.4" version = "1.0.4"
@ -2559,16 +2404,6 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 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]] [[package]]
name = "pem" name = "pem"
version = "3.0.5" version = "3.0.5"
@ -2938,9 +2773,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.20" version = "0.12.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
@ -2955,28 +2790,31 @@ dependencies = [
"hyper-rustls", "hyper-rustls",
"hyper-tls", "hyper-tls",
"hyper-util", "hyper-util",
"ipnet",
"js-sys", "js-sys",
"log", "log",
"mime", "mime",
"native-tls", "native-tls",
"once_cell",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"rustls-pki-types", "rustls-pemfile",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"sync_wrapper", "sync_wrapper",
"system-configuration",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
"tokio-util", "tokio-util",
"tower", "tower",
"tower-http",
"tower-service", "tower-service",
"url", "url",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wasm-streams", "wasm-streams",
"web-sys", "web-sys",
"windows-registry",
] ]
[[package]] [[package]]
@ -3085,6 +2923,15 @@ dependencies = [
"zeroize", "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]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.12.0" version = "1.12.0"
@ -3246,21 +3093,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_yml"
version = "0.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd"
dependencies = [
"indexmap",
"itoa",
"libyml",
"memchr",
"ryu",
"serde",
"version_check",
]
[[package]] [[package]]
name = "sha1" name = "sha1"
version = "0.10.6" version = "0.10.6"
@ -3690,24 +3522,6 @@ dependencies = [
"tower-service", "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]] [[package]]
name = "tower-layer" name = "tower-layer"
version = "0.3.3" version = "0.3.3"
@ -3921,7 +3735,6 @@ dependencies = [
"actix-ws", "actix-ws",
"anyhow", "anyhow",
"basic-jwt", "basic-jwt",
"chrono",
"clap", "clap",
"dotenvy", "dotenvy",
"env_logger", "env_logger",
@ -3941,7 +3754,6 @@ dependencies = [
"rust-embed", "rust-embed",
"serde", "serde",
"serde_json", "serde_json",
"serde_yml",
"sysinfo", "sysinfo",
"tempfile", "tempfile",
"thiserror 2.0.12", "thiserror 2.0.12",
@ -3949,7 +3761,6 @@ dependencies = [
"url", "url",
"uuid", "uuid",
"virt", "virt",
"zip",
] ]
[[package]] [[package]]
@ -4488,20 +4299,6 @@ name = "zeroize"
version = "1.8.1" version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 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]] [[package]]
name = "zerotrie" name = "zerotrie"
@ -4536,50 +4333,6 @@ dependencies = [
"syn", "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]] [[package]]
name = "zstd" name = "zstd"
version = "0.13.3" version = "0.13.3"

View File

@ -8,7 +8,7 @@ edition = "2024"
[dependencies] [dependencies]
log = "0.4.27" log = "0.4.27"
env_logger = "0.11.8" env_logger = "0.11.8"
clap = { version = "4.5.40", features = ["derive", "env"] } clap = { version = "4.5.38", features = ["derive", "env"] }
light-openid = { version = "1.0.4", features = ["crypto-wrapper"] } light-openid = { version = "1.0.4", features = ["crypto-wrapper"] }
lazy_static = "1.5.0" lazy_static = "1.5.0"
actix = "0.13.5" actix = "0.13.5"
@ -19,16 +19,15 @@ actix-identity = "0.8.0"
actix-cors = "0.7.1" actix-cors = "0.7.1"
actix-files = "0.6.6" actix-files = "0.6.6"
actix-ws = "0.3.0" actix-ws = "0.3.0"
actix-http = "3.11.0" actix-http = "3.10.0"
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140" serde_json = "1.0.140"
serde_yml = "0.0.12"
quick-xml = { version = "0.37.5", features = ["serialize", "overlapped-lists"] } quick-xml = { version = "0.37.5", features = ["serialize", "overlapped-lists"] }
futures-util = "0.3.31" futures-util = "0.3.31"
anyhow = "1.0.98" anyhow = "1.0.98"
actix-multipart = "0.7.2" actix-multipart = "0.7.2"
tempfile = "3.20.0" tempfile = "3.20.0"
reqwest = { version = "0.12.20", features = ["stream"] } reqwest = { version = "0.12.15", features = ["stream"] }
url = "2.5.4" url = "2.5.4"
virt = "0.4.2" virt = "0.4.2"
sysinfo = { version = "0.35.1", features = ["serde"] } sysinfo = { version = "0.35.1", features = ["serde"] }
@ -45,5 +44,3 @@ rust-embed = { version = "8.7.2", features = ["mime-guess"] }
dotenvy = "0.15.7" dotenvy = "0.15.7"
nix = { version = "0.30.1", features = ["net"] } nix = { version = "0.30.1", features = ["net"] }
basic-jwt = "0.3.0" basic-jwt = "0.3.0"
zip = "4.1.0"
chrono = "0.4.41"

View File

@ -182,13 +182,6 @@ impl Handler<DeleteDomainReq> for LibVirtActor {
false => sys::VIR_DOMAIN_UNDEFINE_NVRAM, false => sys::VIR_DOMAIN_UNDEFINE_NVRAM,
})?; })?;
// Delete associated cloud init disk
let cloud_init_disk = AppConfig::get().cloud_init_disk_path_for_vm(&domain_name);
if cloud_init_disk.exists() {
std::fs::remove_file(cloud_init_disk)?;
}
// If requested, delete block storage associated with the VM
if !msg.keep_files { if !msg.keep_files {
log::info!("Delete storage associated with the domain"); log::info!("Delete storage associated with the domain");
let path = AppConfig::get().vm_storage_path(msg.id); let path = AppConfig::get().vm_storage_path(msg.id);

View File

@ -250,19 +250,6 @@ impl AppConfig {
self.storage_path().join("iso") self.storage_path().join("iso")
} }
/// Get the path where generated cloud init disk image are stored
pub fn cloud_init_disk_storage_path(&self) -> PathBuf {
self.storage_path().join("cloud_init_disks")
}
/// Get the path where the disk image of a VM is stored
pub fn cloud_init_disk_path_for_vm(&self, name: &str) -> PathBuf {
self.cloud_init_disk_storage_path().join(format!(
"{}-{name}.iso",
constants::CLOUD_INIT_IMAGE_PREFIX_NAME
))
}
/// Get disk images storage directory /// Get disk images storage directory
pub fn disk_images_storage_path(&self) -> PathBuf { pub fn disk_images_storage_path(&self) -> PathBuf {
self.storage_path().join("disk_images") self.storage_path().join("disk_images")

View File

@ -30,9 +30,8 @@ pub const ALLOWED_ISO_MIME_TYPES: [&str; 4] = [
pub const ISO_MAX_SIZE: FileSize = FileSize::from_gb(10); pub const ISO_MAX_SIZE: FileSize = FileSize::from_gb(10);
/// Allowed uploaded disk images formats /// Allowed uploaded disk images formats
pub const ALLOWED_DISK_IMAGES_MIME_TYPES: [&str; 4] = [ pub const ALLOWED_DISK_IMAGES_MIME_TYPES: [&str; 3] = [
"application/x-qemu-disk", "application/x-qemu-disk",
"application/x-raw-disk-image",
"application/gzip", "application/gzip",
"application/octet-stream", "application/octet-stream",
]; ];
@ -58,9 +57,6 @@ pub const DISK_SIZE_MIN: FileSize = FileSize::from_mb(50);
/// Disk size max (B) /// Disk size max (B)
pub const DISK_SIZE_MAX: FileSize = FileSize::from_gb(20000); pub const DISK_SIZE_MAX: FileSize = FileSize::from_gb(20000);
/// Cloud init generated disk image prefix
pub const CLOUD_INIT_IMAGE_PREFIX_NAME: &str = "virtweb-cloudinit-autogen-image";
/// Net nat entry comment max size /// Net nat entry comment max size
pub const NET_NAT_COMMENT_MAX_SIZE: usize = 250; pub const NET_NAT_COMMENT_MAX_SIZE: usize = 250;
@ -126,25 +122,19 @@ pub const API_TOKEN_DESCRIPTION_MAX_LENGTH: usize = 30;
pub const API_TOKEN_RIGHT_PATH_MAX_LENGTH: usize = 255; pub const API_TOKEN_RIGHT_PATH_MAX_LENGTH: usize = 255;
/// Qemu image program path /// Qemu image program path
pub const PROGRAM_QEMU_IMAGE: &str = "/usr/bin/qemu-img"; pub const QEMU_IMAGE_PROGRAM: &str = "/usr/bin/qemu-img";
/// IP program path /// IP program path
pub const PROGRAM_IP: &str = "/usr/sbin/ip"; pub const IP_PROGRAM: &str = "/usr/sbin/ip";
/// Copy program path /// Copy program path
pub const PROGRAM_COPY: &str = "/bin/cp"; pub const COPY_PROGRAM: &str = "/bin/cp";
/// Gzip program path /// Gzip program path
pub const PROGRAM_GZIP: &str = "/usr/bin/gzip"; pub const GZIP_PROGRAM: &str = "/usr/bin/gzip";
/// XZ program path
pub const PROGRAM_XZ: &str = "/usr/bin/xz";
/// Bash program /// Bash program
pub const PROGRAM_BASH: &str = "/usr/bin/bash"; pub const BASH_PROGRAM: &str = "/usr/bin/bash";
/// DD program /// DD program
pub const PROGRAM_DD: &str = "/usr/bin/dd"; pub const DD_PROGRAM: &str = "/usr/bin/dd";
/// cloud-localds program
pub const PROGRAM_CLOUD_LOCALDS: &str = "/usr/bin/cloud-localds";

View File

@ -55,15 +55,7 @@ pub async fn upload(MultipartForm(mut form): MultipartForm<UploadDiskImageForm>)
} }
// Copy the file to the destination // Copy the file to the destination
file.file.persist(&dest_path)?; file.file.persist(dest_path)?;
// Check if file information can be loaded
if let Err(e) = DiskFileInfo::load_file(&dest_path) {
log::error!("Failed to get information about uploaded disk file! {e}");
std::fs::remove_file(&dest_path)?;
return Ok(HttpResponse::InternalServerError()
.json(format!("Unable to process uploaded file! {e}")));
}
Ok(HttpResponse::Ok().json("Successfully uploaded disk image!")) Ok(HttpResponse::Ok().json("Successfully uploaded disk image!"))
} }
@ -197,46 +189,6 @@ pub async fn handle_convert_request(
Ok(HttpResponse::Accepted().json("Successfully converted disk file")) Ok(HttpResponse::Accepted().json("Successfully converted disk file"))
} }
#[derive(serde::Deserialize)]
pub struct RenameDiskImageRequest {
name: String,
}
/// Rename disk image
pub async fn rename(
p: web::Path<DiskFilePath>,
req: web::Json<RenameDiskImageRequest>,
) -> HttpResult {
// Check source
if !files_utils::check_file_name(&p.filename) {
return Ok(HttpResponse::BadRequest().json("Invalid src file name!"));
}
let src_path = AppConfig::get().disk_images_file_path(&p.filename);
if !src_path.exists() {
return Ok(HttpResponse::NotFound().json("Disk image does not exists!"));
}
// Check destination
if !files_utils::check_file_name(&req.name) {
return Ok(HttpResponse::BadRequest().json("Invalid dst file name!"));
}
let dst_path = AppConfig::get().disk_images_file_path(&req.name);
if dst_path.exists() {
return Ok(HttpResponse::Conflict().json("Destination name already exists!"));
}
// Check extension
let disk = DiskFileInfo::load_file(&src_path)?;
if !disk.format.ext().iter().any(|e| req.name.ends_with(e)) {
return Ok(HttpResponse::BadRequest().json("Invalid destination file extension!"));
}
// Perform rename
std::fs::rename(&src_path, &dst_path)?;
Ok(HttpResponse::Accepted().finish())
}
/// Delete a disk image /// Delete a disk image
pub async fn delete(p: web::Path<DiskFilePath>) -> HttpResult { pub async fn delete(p: web::Path<DiskFilePath>) -> HttpResult {
if !files_utils::check_file_name(&p.filename) { if !files_utils::check_file_name(&p.filename) {

View File

@ -4,7 +4,6 @@ use actix_web::body::BoxBody;
use actix_web::{HttpResponse, web}; use actix_web::{HttpResponse, web};
use std::error::Error; use std::error::Error;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use zip::result::ZipError;
pub mod api_tokens_controller; pub mod api_tokens_controller;
pub mod auth_controller; pub mod auth_controller;
@ -103,12 +102,6 @@ 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 { impl From<HttpResponse> for HttpErr {
fn from(value: HttpResponse) -> Self { fn from(value: HttpResponse) -> Self {
HttpErr::HTTPResponse(value) HttpErr::HTTPResponse(value)

View File

@ -1,24 +1,14 @@
use crate::actors::vnc_tokens_actor::VNC_TOKEN_LIFETIME; use crate::actors::vnc_tokens_actor::VNC_TOKEN_LIFETIME;
use crate::app_config::AppConfig; 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::constants::{DISK_NAME_MAX_LEN, DISK_NAME_MIN_LEN, DISK_SIZE_MAX, DISK_SIZE_MIN};
use crate::controllers::{HttpResult, LibVirtReq}; use crate::controllers::{HttpResult, LibVirtReq};
use crate::extractors::local_auth_extractor::LocalAuthEnabled; use crate::extractors::local_auth_extractor::LocalAuthEnabled;
use crate::libvirt_rest_structures::hypervisor::HypervisorInfo; 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::nat::nat_hook;
use crate::utils::net_utils; use crate::utils::net_utils;
use crate::utils::time_utils::{format_date, time}; use actix_web::{HttpResponse, Responder};
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 sysinfo::{Components, Disks, Networks, System};
use zip::ZipWriter;
use zip::write::SimpleFileOptions;
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
struct StaticConfig { struct StaticConfig {
@ -209,85 +199,3 @@ pub async fn networks_list() -> HttpResult {
pub async fn bridges_list() -> HttpResult { pub async fn bridges_list() -> HttpResult {
Ok(HttpResponse::Ok().json(net_utils::bridges_list()?)) 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

@ -109,28 +109,6 @@ pub async fn get_single_src_def(client: LibVirtReq, id: web::Path<SingleVMUUidRe
.body(info)) .body(info))
} }
/// Get the generated cloud init configuration disk of a vm
pub async fn get_cloud_init_disk(client: LibVirtReq, id: web::Path<SingleVMUUidReq>) -> HttpResult {
let info = match client.get_single_domain(id.uid).await {
Ok(i) => i,
Err(e) => {
log::error!("Failed to get domain information! {e}");
return Ok(HttpResponse::InternalServerError().json(e.to_string()));
}
};
let vm = VMInfo::from_domain(info)?;
let disk = vm.cloud_init.generate_nocloud_disk()?;
Ok(HttpResponse::Ok()
.content_type("application/x-iso9660-image")
.insert_header((
"Content-Disposition",
format!("attachment; filename=\"cloud_init_{}.iso\"", vm.name),
))
.body(disk))
}
/// Update a VM information /// Update a VM information
pub async fn update( pub async fn update(
client: LibVirtReq, client: LibVirtReq,

View File

@ -1,5 +1,4 @@
use crate::libvirt_lib_structures::XMLUuid; use crate::libvirt_lib_structures::XMLUuid;
use crate::utils::cloud_init_utils::CloudInitConfig;
/// VirtWeb specific metadata /// VirtWeb specific metadata
#[derive(serde::Serialize, serde::Deserialize, Default, Debug, Clone)] #[derive(serde::Serialize, serde::Deserialize, Default, Debug, Clone)]
@ -9,8 +8,6 @@ pub struct DomainMetadataVirtWebXML {
pub ns: String, pub ns: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub group: Option<String>, pub group: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cloud_init: Option<CloudInitConfig>,
} }
/// Domain metadata /// Domain metadata
@ -25,13 +22,10 @@ pub struct DomainMetadataXML {
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "os")] #[serde(rename = "os")]
pub struct OSXML { pub struct OSXML {
#[serde(rename = "@firmware", default, skip_serializing_if = "Option::is_none")] #[serde(rename = "@firmware", default)]
pub firmware: Option<String>, pub firmware: String,
pub r#type: OSTypeXML, pub r#type: OSTypeXML,
#[serde(skip_serializing_if = "Option::is_none")]
pub loader: Option<OSLoaderXML>, pub loader: Option<OSLoaderXML>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bootmenu: Option<OSBootMenuXML>,
pub smbios: Option<OSSMBiosXML>, pub smbios: Option<OSSMBiosXML>,
} }
@ -55,16 +49,6 @@ pub struct OSLoaderXML {
pub secure: String, pub secure: String,
} }
/// Legacy boot menu information
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "bootmenu")]
pub struct OSBootMenuXML {
#[serde(rename = "@enable")]
pub enable: String,
#[serde(rename = "@timeout")]
pub timeout: usize,
}
/// SMBIOS System information /// SMBIOS System information
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "smbios")] #[serde(rename = "smbios")]

View File

@ -13,6 +13,4 @@ enum LibVirtStructError {
ParseFilteringChain(String), ParseFilteringChain(String),
#[error("NetworkFilterExtractionError: {0}")] #[error("NetworkFilterExtractionError: {0}")]
NetworkFilterExtraction(String), NetworkFilterExtraction(String),
#[error("CloudInitConfigurationError: {0}")]
CloudInitConfiguration(String),
} }

View File

@ -3,13 +3,10 @@ use crate::constants;
use crate::libvirt_lib_structures::XMLUuid; use crate::libvirt_lib_structures::XMLUuid;
use crate::libvirt_lib_structures::domain::*; use crate::libvirt_lib_structures::domain::*;
use crate::libvirt_rest_structures::LibVirtStructError; use crate::libvirt_rest_structures::LibVirtStructError;
use crate::libvirt_rest_structures::LibVirtStructError::{ use crate::libvirt_rest_structures::LibVirtStructError::StructureExtraction;
CloudInitConfiguration, StructureExtraction,
};
use crate::utils::cloud_init_utils::CloudInitConfig;
use crate::utils::file_size_utils::FileSize; use crate::utils::file_size_utils::FileSize;
use crate::utils::files_utils; use crate::utils::files_utils;
use crate::utils::vm_file_disks_utils::{VMDiskBus, VMDiskFormat, VMFileDisk}; use crate::utils::vm_file_disks_utils::{VMDiskFormat, VMFileDisk};
use lazy_regex::regex; use lazy_regex::regex;
use num::Integer; use num::Integer;
@ -20,7 +17,6 @@ pub struct VMGroupId(pub String);
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
pub enum BootType { pub enum BootType {
Legacy,
UEFI, UEFI,
UEFISecureBoot, UEFISecureBoot,
} }
@ -97,9 +93,6 @@ pub struct VMInfo {
pub tpm_module: bool, pub tpm_module: bool,
/// Strings injected as OEM Strings in SMBios configuration /// Strings injected as OEM Strings in SMBios configuration
pub oem_strings: Vec<String>, pub oem_strings: Vec<String>,
/// Cloud init configuration
#[serde(default)]
pub cloud_init: CloudInitConfig,
} }
impl VMInfo { impl VMInfo {
@ -144,26 +137,9 @@ impl VMInfo {
return Err(StructureExtraction("Invalid number of vCPU specified!").into()); return Err(StructureExtraction("Invalid number of vCPU specified!").into());
} }
if let Some(e) = self.cloud_init.check_error() { let mut disks = vec![];
return Err(CloudInitConfiguration(e).into());
}
let mut iso_absolute_files = vec![]; // Add ISO files
// Process cloud init image
if self.cloud_init.attach_config {
let cloud_init_disk_path = AppConfig::get().cloud_init_disk_path_for_vm(&self.name);
// Apply latest cloud init configuration
std::fs::write(
&cloud_init_disk_path,
self.cloud_init.generate_nocloud_disk()?,
)?;
iso_absolute_files.push(cloud_init_disk_path);
}
// Process uploaded ISO files
for iso_file in &self.iso_files { for iso_file in &self.iso_files {
if !files_utils::check_file_name(iso_file) { if !files_utils::check_file_name(iso_file) {
return Err(StructureExtraction("ISO filename is invalid!").into()); return Err(StructureExtraction("ISO filename is invalid!").into());
@ -175,13 +151,6 @@ impl VMInfo {
return Err(StructureExtraction("Specified ISO file does not exists!").into()); return Err(StructureExtraction("Specified ISO file does not exists!").into());
} }
iso_absolute_files.push(path);
}
let mut disks = vec![];
// Add ISO disk files
for iso_path in iso_absolute_files {
disks.push(DiskXML { disks.push(DiskXML {
r#type: "file".to_string(), r#type: "file".to_string(),
device: "cdrom".to_string(), device: "cdrom".to_string(),
@ -191,7 +160,7 @@ impl VMInfo {
cache: "none".to_string(), cache: "none".to_string(),
}, },
source: DiskSourceXML { source: DiskSourceXML {
file: iso_path.to_string_lossy().to_string(), file: path.to_string_lossy().to_string(),
}, },
target: DiskTargetXML { target: DiskTargetXML {
dev: format!( dev: format!(
@ -208,7 +177,6 @@ impl VMInfo {
}) })
} }
// Configure VNC access, if requested
let (vnc_graphics, vnc_video) = match self.vnc_access { let (vnc_graphics, vnc_video) = match self.vnc_access {
true => ( true => (
Some(GraphicsXML { Some(GraphicsXML {
@ -345,11 +313,7 @@ impl VMInfo {
"vd{}", "vd{}",
["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"][disks.len()] ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"][disks.len()]
), ),
bus: match disk.bus { bus: "virtio".to_string(),
VMDiskBus::Virtio => "virtio",
VMDiskBus::SATA => "sata",
}
.to_string(),
}, },
readonly: None, readonly: None,
boot: DiskBootXML { boot: DiskBootXML {
@ -371,7 +335,6 @@ impl VMInfo {
virtweb: DomainMetadataVirtWebXML { virtweb: DomainMetadataVirtWebXML {
ns: "https://virtweb.communiquons.org".to_string(), ns: "https://virtweb.communiquons.org".to_string(),
group: self.group.clone().map(|g| g.0), group: self.group.clone().map(|g| g.0),
cloud_init: Some(self.cloud_init.clone()),
}, },
}), }),
os: OSXML { os: OSXML {
@ -384,26 +347,13 @@ impl VMInfo {
machine: "q35".to_string(), machine: "q35".to_string(),
body: "hvm".to_string(), body: "hvm".to_string(),
}, },
firmware: match self.boot_type { firmware: "efi".to_string(),
BootType::Legacy => None, loader: Some(OSLoaderXML {
_ => Some("efi".to_string()), secure: match self.boot_type {
}, BootType::UEFI => "no".to_string(),
loader: match self.boot_type { BootType::UEFISecureBoot => "yes".to_string(),
BootType::Legacy => None, },
_ => Some(OSLoaderXML { }),
secure: match self.boot_type {
BootType::UEFISecureBoot => "yes".to_string(),
_ => "no".to_string(),
},
}),
},
bootmenu: match self.boot_type {
BootType::Legacy => Some(OSBootMenuXML {
enable: "yes".to_string(),
timeout: 3000,
}),
_ => None,
},
smbios: Some(OSSMBiosXML { smbios: Some(OSSMBiosXML {
mode: "sysinfo".to_string(), mode: "sysinfo".to_string(),
}), }),
@ -495,10 +445,9 @@ impl VMInfo {
.virtweb .virtweb
.group .group
.map(VMGroupId), .map(VMGroupId),
boot_type: match (domain.os.loader, domain.os.bootmenu) { boot_type: match domain.os.loader {
(_, Some(_)) => BootType::Legacy, None => BootType::UEFI,
(None, _) => BootType::UEFI, Some(l) => match l.secure.as_str() {
(Some(l), _) => match l.secure.as_str() {
"yes" => BootType::UEFISecureBoot, "yes" => BootType::UEFISecureBoot,
_ => BootType::UEFI, _ => BootType::UEFI,
}, },
@ -522,7 +471,6 @@ impl VMInfo {
.iter() .iter()
.filter(|d| d.device == "cdrom") .filter(|d| d.device == "cdrom")
.map(|d| d.source.file.rsplit_once('/').unwrap().1.to_string()) .map(|d| d.source.file.rsplit_once('/').unwrap().1.to_string())
.filter(|d| !d.starts_with(constants::CLOUD_INIT_IMAGE_PREFIX_NAME))
.collect(), .collect(),
file_disks: domain file_disks: domain
@ -531,7 +479,7 @@ impl VMInfo {
.iter() .iter()
.filter(|d| d.device == "disk") .filter(|d| d.device == "disk")
.map(|d| { .map(|d| {
VMFileDisk::load_from_file(&d.source.file, &d.target.bus) VMFileDisk::load_from_file(&d.source.file)
.expect("Failed to load file disk information!") .expect("Failed to load file disk information!")
}) })
.collect(), .collect(),
@ -615,13 +563,6 @@ impl VMInfo {
.and_then(|s| s.oem_strings) .and_then(|s| s.oem_strings)
.map(|s| s.entries.iter().map(|o| o.content.to_string()).collect()) .map(|s| s.entries.iter().map(|o| o.content.to_string()).collect())
.unwrap_or_default(), .unwrap_or_default(),
cloud_init: domain
.metadata
.clone()
.unwrap_or_default()
.virtweb
.cloud_init
.unwrap_or_default(),
}) })
} }
} }

View File

@ -47,22 +47,16 @@ async fn main() -> std::io::Result<()> {
log::debug!("Checking for required programs"); log::debug!("Checking for required programs");
exec_utils::check_program( exec_utils::check_program(
constants::PROGRAM_QEMU_IMAGE, constants::QEMU_IMAGE_PROGRAM,
"QEMU disk image utility is required to manipulate QCow2 files!", "QEMU disk image utility is required to manipulate QCow2 files!",
); );
exec_utils::check_program( exec_utils::check_program(
constants::PROGRAM_IP, constants::IP_PROGRAM,
"ip is required to access bridges information!", "ip is required to access bridges information!",
); );
exec_utils::check_program(
constants::PROGRAM_CLOUD_LOCALDS,
"cloud-localds from package cloud-image-utils is required to build cloud-init images!",
);
log::debug!("Create required directory, if missing"); log::debug!("Create required directory, if missing");
files_utils::create_directory_if_missing(AppConfig::get().iso_storage_path()).unwrap(); files_utils::create_directory_if_missing(AppConfig::get().iso_storage_path()).unwrap();
files_utils::create_directory_if_missing(AppConfig::get().cloud_init_disk_storage_path())
.unwrap();
files_utils::create_directory_if_missing(AppConfig::get().disk_images_storage_path()).unwrap(); files_utils::create_directory_if_missing(AppConfig::get().disk_images_storage_path()).unwrap();
files_utils::create_directory_if_missing(AppConfig::get().vnc_sockets_path()).unwrap(); 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::set_file_permission(AppConfig::get().vnc_sockets_path(), 0o777).unwrap();
@ -157,10 +151,6 @@ async fn main() -> std::io::Result<()> {
"/api/server/bridges", "/api/server/bridges",
web::get().to(server_controller::bridges_list), web::get().to(server_controller::bridges_list),
) )
.route(
"/api/server/export_configs",
web::get().to(server_controller::export_all_configs),
)
// Auth controller // Auth controller
.route( .route(
"/api/auth/local", "/api/auth/local",
@ -208,10 +198,6 @@ async fn main() -> std::io::Result<()> {
"/api/vm/{uid}/src", "/api/vm/{uid}/src",
web::get().to(vm_controller::get_single_src_def), web::get().to(vm_controller::get_single_src_def),
) )
.route(
"/api/vm/{uid}/cloud_init_disk",
web::get().to(vm_controller::get_cloud_init_disk),
)
.route( .route(
"/api/vm/{uid}/autostart", "/api/vm/{uid}/autostart",
web::get().to(vm_controller::get_autostart), web::get().to(vm_controller::get_autostart),
@ -366,10 +352,6 @@ async fn main() -> std::io::Result<()> {
"/api/disk_images/{filename}/convert", "/api/disk_images/{filename}/convert",
web::post().to(disk_images_controller::convert), web::post().to(disk_images_controller::convert),
) )
.route(
"/api/disk_images/{filename}/rename",
web::post().to(disk_images_controller::rename),
)
.route( .route(
"/api/disk_images/{filename}", "/api/disk_images/{filename}",
web::delete().to(disk_images_controller::delete), web::delete().to(disk_images_controller::delete),

View File

@ -1,117 +0,0 @@
use crate::app_config::AppConfig;
use crate::constants;
use std::process::Command;
/// Cloud init DS Mode
#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum CloudInitDSMode {
/// Networking is required
Net,
/// Does not require networking to be up before user-data actions are run
Local,
}
/// VM Cloud Init configuration
///
/// RedHat documentation: https://docs.redhat.com/fr/documentation/red_hat_enterprise_linux/9/html/configuring_and_managing_cloud-init_for_rhel_9/configuring-cloud-init_cloud-content
/// cloud-localds source code: https://github.com/canonical/cloud-utils/blob/main/bin/cloud-localds
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Default)]
pub struct CloudInitConfig {
pub attach_config: bool,
/// Main user data
pub user_data: String,
/// Instance ID, set in metadata file
#[serde(skip_serializing_if = "Option::is_none")]
pub instance_id: Option<String>,
/// Local hostname, set in metadata file
#[serde(skip_serializing_if = "Option::is_none")]
pub local_hostname: Option<String>,
/// Data source mode
#[serde(skip_serializing_if = "Option::is_none")]
pub dsmode: Option<CloudInitDSMode>,
/// Network configuration
#[serde(skip_serializing_if = "Option::is_none")]
pub network_configuration: Option<String>,
}
impl CloudInitConfig {
/// Check cloud init configuration
pub fn check_error(&self) -> Option<String> {
if !self.user_data.is_empty() {
// Check YAML content
if let Err(e) = serde_yml::from_str::<serde_json::Value>(&self.user_data) {
return Some(format!(
"user data is an invalid YAML file! Deserialization error: {e}"
));
}
// Check first line
if !self.user_data.starts_with("#cloud-config\n") {
return Some(
"user data file MUST start with '#cloud-config' as first line!".to_string(),
);
}
}
None
}
/// Generate disk image for nocloud usage
pub fn generate_nocloud_disk(&self) -> anyhow::Result<Vec<u8>> {
let temp_path = tempfile::tempdir_in(&AppConfig::get().temp_dir)?;
let mut cmd = Command::new(constants::PROGRAM_CLOUD_LOCALDS);
// ISO destination path
let temp_iso = temp_path.path().join("disk.iso");
cmd.arg(&temp_iso);
// Process network configuration
if let Some(net_conf) = &self.network_configuration {
let net_conf_path = temp_path.path().join("network");
std::fs::write(&net_conf_path, net_conf)?;
cmd.arg("--network-config").arg(&net_conf_path);
}
// Process user data
let user_data_path = temp_path.path().join("user-data");
std::fs::write(&user_data_path, &self.user_data)?;
cmd.arg(user_data_path);
// Process metadata
let mut metadatas = vec![];
if let Some(inst_id) = &self.instance_id {
metadatas.push(format!("instance-id: {}", inst_id));
}
if let Some(local_hostname) = &self.local_hostname {
metadatas.push(format!("local-hostname: {}", local_hostname));
}
if let Some(dsmode) = &self.dsmode {
metadatas.push(format!(
"dsmode: {}",
match dsmode {
CloudInitDSMode::Net => "net",
CloudInitDSMode::Local => "local",
}
));
}
let meta_data_path = temp_path.path().join("meta-data");
std::fs::write(&meta_data_path, metadatas.join("\n"))?;
cmd.arg(meta_data_path);
// Execute command
let output = cmd.output()?;
if !output.status.success() {
anyhow::bail!(
"{} exited with status {}!\nStdout: {}\nStderr: {}",
constants::PROGRAM_CLOUD_LOCALDS,
output.status,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
// Read generated ISO file
Ok(std::fs::read(temp_iso)?)
}
}

View File

@ -28,10 +28,8 @@ pub enum DiskFileFormat {
#[serde(default)] #[serde(default)]
virtual_size: FileSize, virtual_size: FileSize,
}, },
GzCompressedRaw, CompressedRaw,
GzCompressedQCow2, CompressedQCow2,
XzCompressedRaw,
XzCompressedQCow2,
} }
impl DiskFileFormat { impl DiskFileFormat {
@ -39,10 +37,8 @@ impl DiskFileFormat {
match self { match self {
DiskFileFormat::Raw { .. } => &["raw", ""], DiskFileFormat::Raw { .. } => &["raw", ""],
DiskFileFormat::QCow2 { .. } => &["qcow2"], DiskFileFormat::QCow2 { .. } => &["qcow2"],
DiskFileFormat::GzCompressedRaw => &["raw.gz"], DiskFileFormat::CompressedRaw => &["raw.gz"],
DiskFileFormat::GzCompressedQCow2 => &["qcow2.gz"], DiskFileFormat::CompressedQCow2 => &["qcow2.gz"],
DiskFileFormat::XzCompressedRaw => &["raw.xz"],
DiskFileFormat::XzCompressedQCow2 => &["qcow2.xz"],
} }
} }
} }
@ -85,14 +81,9 @@ impl DiskFileInfo {
}, },
"gz" if name.ends_with(".qcow2") => { "gz" if name.ends_with(".qcow2") => {
name = name.strip_suffix(".qcow2").unwrap_or(&name).to_string(); name = name.strip_suffix(".qcow2").unwrap_or(&name).to_string();
DiskFileFormat::GzCompressedQCow2 DiskFileFormat::CompressedQCow2
} }
"gz" => DiskFileFormat::GzCompressedRaw, "gz" => DiskFileFormat::CompressedRaw,
"xz" if name.ends_with(".qcow2") => {
name = name.strip_suffix(".qcow2").unwrap_or(&name).to_string();
DiskFileFormat::XzCompressedQCow2
}
"xz" => DiskFileFormat::XzCompressedRaw,
_ => anyhow::bail!("Unsupported disk extension: {ext}!"), _ => anyhow::bail!("Unsupported disk extension: {ext}!"),
}; };
@ -133,7 +124,7 @@ impl DiskFileInfo {
} }
DiskFileFormat::QCow2 { virtual_size } => { DiskFileFormat::QCow2 { virtual_size } => {
let mut cmd = Command::new(constants::PROGRAM_QEMU_IMAGE); let mut cmd = Command::new(constants::QEMU_IMAGE_PROGRAM);
cmd.arg("create") cmd.arg("create")
.arg("-f") .arg("-f")
.arg("qcow2") .arg("qcow2")
@ -168,9 +159,9 @@ impl DiskFileInfo {
// Prepare the conversion // Prepare the conversion
let mut cmd = match (self.format, dest_format) { let mut cmd = match (self.format, dest_format) {
// Decompress QCow2 (GZIP) // Decompress QCow2
(DiskFileFormat::GzCompressedQCow2, DiskFileFormat::QCow2 { .. }) => { (DiskFileFormat::CompressedQCow2, DiskFileFormat::QCow2 { .. }) => {
let mut cmd = Command::new(constants::PROGRAM_GZIP); let mut cmd = Command::new(constants::GZIP_PROGRAM);
cmd.arg("--keep") cmd.arg("--keep")
.arg("--decompress") .arg("--decompress")
.arg("--to-stdout") .arg("--to-stdout")
@ -179,30 +170,9 @@ impl DiskFileInfo {
cmd cmd
} }
// Decompress QCow2 (XZ) // Compress QCow2
(DiskFileFormat::XzCompressedQCow2, DiskFileFormat::QCow2 { .. }) => { (DiskFileFormat::QCow2 { .. }, DiskFileFormat::CompressedQCow2) => {
let mut cmd = Command::new(constants::PROGRAM_XZ); let mut cmd = Command::new(constants::GZIP_PROGRAM);
cmd.arg("--stdout")
.arg("--keep")
.arg("--decompress")
.arg(&self.file_path)
.stdout(File::create(&temp_file)?);
cmd
}
// Compress QCow2 (Gzip)
(DiskFileFormat::QCow2 { .. }, DiskFileFormat::GzCompressedQCow2) => {
let mut cmd = Command::new(constants::PROGRAM_GZIP);
cmd.arg("--keep")
.arg("--to-stdout")
.arg(&self.file_path)
.stdout(File::create(&temp_file)?);
cmd
}
// Compress QCow2 (Xz)
(DiskFileFormat::QCow2 { .. }, DiskFileFormat::XzCompressedQCow2) => {
let mut cmd = Command::new(constants::PROGRAM_XZ);
cmd.arg("--keep") cmd.arg("--keep")
.arg("--to-stdout") .arg("--to-stdout")
.arg(&self.file_path) .arg(&self.file_path)
@ -212,14 +182,8 @@ impl DiskFileInfo {
// Convert QCow2 to Raw file // Convert QCow2 to Raw file
(DiskFileFormat::QCow2 { .. }, DiskFileFormat::Raw { is_sparse }) => { (DiskFileFormat::QCow2 { .. }, DiskFileFormat::Raw { is_sparse }) => {
let mut cmd = Command::new(constants::PROGRAM_QEMU_IMAGE); let mut cmd = Command::new(constants::QEMU_IMAGE_PROGRAM);
cmd.arg("convert") cmd.arg("convert").arg(&self.file_path).arg(&temp_file);
.arg("-f")
.arg("qcow2")
.arg("-O")
.arg("raw")
.arg(&self.file_path)
.arg(&temp_file);
if !is_sparse { if !is_sparse {
cmd.args(["-S", "0"]); cmd.args(["-S", "0"]);
@ -231,10 +195,8 @@ impl DiskFileInfo {
// Clone a QCow file, using qemu-image instead of cp might improve "sparsification" of // Clone a QCow file, using qemu-image instead of cp might improve "sparsification" of
// file // file
(DiskFileFormat::QCow2 { .. }, DiskFileFormat::QCow2 { .. }) => { (DiskFileFormat::QCow2 { .. }, DiskFileFormat::QCow2 { .. }) => {
let mut cmd = Command::new(constants::PROGRAM_QEMU_IMAGE); let mut cmd = Command::new(constants::QEMU_IMAGE_PROGRAM);
cmd.arg("convert") cmd.arg("convert")
.arg("-f")
.arg("qcow2")
.arg("-O") .arg("-O")
.arg("qcow2") .arg("qcow2")
.arg(&self.file_path) .arg(&self.file_path)
@ -244,21 +206,15 @@ impl DiskFileInfo {
// Convert Raw to QCow2 file // Convert Raw to QCow2 file
(DiskFileFormat::Raw { .. }, DiskFileFormat::QCow2 { .. }) => { (DiskFileFormat::Raw { .. }, DiskFileFormat::QCow2 { .. }) => {
let mut cmd = Command::new(constants::PROGRAM_QEMU_IMAGE); let mut cmd = Command::new(constants::QEMU_IMAGE_PROGRAM);
cmd.arg("convert") cmd.arg("convert").arg(&self.file_path).arg(&temp_file);
.arg("-f")
.arg("raw")
.arg("-O")
.arg("qcow2")
.arg(&self.file_path)
.arg(&temp_file);
cmd cmd
} }
// Render raw file non sparse // Render raw file non sparse
(DiskFileFormat::Raw { is_sparse: true }, DiskFileFormat::Raw { is_sparse: false }) => { (DiskFileFormat::Raw { is_sparse: true }, DiskFileFormat::Raw { is_sparse: false }) => {
let mut cmd = Command::new(constants::PROGRAM_COPY); let mut cmd = Command::new(constants::COPY_PROGRAM);
cmd.arg("--sparse=never") cmd.arg("--sparse=never")
.arg(&self.file_path) .arg(&self.file_path)
.arg(&temp_file); .arg(&temp_file);
@ -267,16 +223,16 @@ impl DiskFileInfo {
// Render raw file sparse // Render raw file sparse
(DiskFileFormat::Raw { is_sparse: false }, DiskFileFormat::Raw { is_sparse: true }) => { (DiskFileFormat::Raw { is_sparse: false }, DiskFileFormat::Raw { is_sparse: true }) => {
let mut cmd = Command::new(constants::PROGRAM_DD); let mut cmd = Command::new(constants::DD_PROGRAM);
cmd.arg("conv=sparse") cmd.arg("conv=sparse")
.arg(format!("if={}", self.file_path.display())) .arg(format!("if={}", self.file_path.display()))
.arg(format!("of={}", temp_file.display())); .arg(format!("of={}", temp_file.display()));
cmd cmd
} }
// Compress Raw (Gz) // Compress Raw
(DiskFileFormat::Raw { .. }, DiskFileFormat::GzCompressedRaw) => { (DiskFileFormat::Raw { .. }, DiskFileFormat::CompressedRaw) => {
let mut cmd = Command::new(constants::PROGRAM_GZIP); let mut cmd = Command::new(constants::GZIP_PROGRAM);
cmd.arg("--keep") cmd.arg("--keep")
.arg("--to-stdout") .arg("--to-stdout")
.arg(&self.file_path) .arg(&self.file_path)
@ -284,29 +240,9 @@ impl DiskFileInfo {
cmd cmd
} }
// Compress Raw (Xz) // Decompress Raw to not sparse file
(DiskFileFormat::Raw { .. }, DiskFileFormat::XzCompressedRaw) => { (DiskFileFormat::CompressedRaw, DiskFileFormat::Raw { is_sparse: false }) => {
let mut cmd = Command::new(constants::PROGRAM_XZ); let mut cmd = Command::new(constants::GZIP_PROGRAM);
cmd.arg("--keep")
.arg("--to-stdout")
.arg(&self.file_path)
.stdout(File::create(&temp_file)?);
cmd
}
// Decompress Raw (Gz) to not sparse file
(DiskFileFormat::GzCompressedRaw, DiskFileFormat::Raw { is_sparse: false }) => {
let mut cmd = Command::new(constants::PROGRAM_GZIP);
cmd.arg("--keep")
.arg("--decompress")
.arg("--to-stdout")
.arg(&self.file_path)
.stdout(File::create(&temp_file)?);
cmd
}
// Decompress Raw (Xz) to not sparse file
(DiskFileFormat::XzCompressedRaw, DiskFileFormat::Raw { is_sparse: false }) => {
let mut cmd = Command::new(constants::PROGRAM_XZ);
cmd.arg("--keep") cmd.arg("--keep")
.arg("--decompress") .arg("--decompress")
.arg("--to-stdout") .arg("--to-stdout")
@ -315,29 +251,15 @@ impl DiskFileInfo {
cmd cmd
} }
// Decompress Raw (Gz) to sparse file // Decompress Raw to sparse file
// https://benou.fr/www/ben/decompressing-sparse-files.html // https://benou.fr/www/ben/decompressing-sparse-files.html
(DiskFileFormat::GzCompressedRaw, DiskFileFormat::Raw { is_sparse: true }) => { (DiskFileFormat::CompressedRaw, DiskFileFormat::Raw { is_sparse: true }) => {
let mut cmd = Command::new(constants::PROGRAM_BASH); let mut cmd = Command::new(constants::BASH_PROGRAM);
cmd.arg("-c").arg(format!( cmd.arg("-c").arg(format!(
"{} --decompress --to-stdout {} | {} conv=sparse of={}", "{} -d -c {} | {} conv=sparse of={}",
constants::PROGRAM_GZIP, constants::GZIP_PROGRAM,
self.file_path.display(), self.file_path.display(),
constants::PROGRAM_DD, constants::DD_PROGRAM,
temp_file.display()
));
cmd
}
// Decompress Raw (XZ) to sparse file
// https://benou.fr/www/ben/decompressing-sparse-files.html
(DiskFileFormat::XzCompressedRaw, DiskFileFormat::Raw { is_sparse: true }) => {
let mut cmd = Command::new(constants::PROGRAM_BASH);
cmd.arg("-c").arg(format!(
"{} --decompress --to-stdout {} | {} conv=sparse of={}",
constants::PROGRAM_XZ,
self.file_path.display(),
constants::PROGRAM_DD,
temp_file.display() temp_file.display()
)); ));
cmd cmd
@ -345,7 +267,7 @@ impl DiskFileInfo {
// Dumb copy of file // Dumb copy of file
(a, b) if a == b => { (a, b) if a == b => {
let mut cmd = Command::new(constants::PROGRAM_COPY); let mut cmd = Command::new(constants::COPY_PROGRAM);
cmd.arg("--sparse=auto") cmd.arg("--sparse=auto")
.arg(&self.file_path) .arg(&self.file_path)
.arg(&temp_file); .arg(&temp_file);
@ -394,44 +316,6 @@ impl DiskFileInfo {
Ok(()) Ok(())
} }
/// Get disk virtual size, if available
pub fn virtual_size(&self) -> Option<FileSize> {
match self.format {
DiskFileFormat::Raw { .. } => Some(self.file_size),
DiskFileFormat::QCow2 { virtual_size } => Some(virtual_size),
_ => None,
}
}
/// Resize disk
pub fn resize(&self, new_size: FileSize) -> anyhow::Result<()> {
if new_size <= self.virtual_size().unwrap_or(new_size) {
anyhow::bail!("Shrinking disk image file is not supported!");
}
let mut cmd = Command::new(constants::PROGRAM_QEMU_IMAGE);
cmd.arg("resize")
.arg("-f")
.arg(match self.format {
DiskFileFormat::QCow2 { .. } => "qcow2",
DiskFileFormat::Raw { .. } => "raw",
f => anyhow::bail!("Unsupported disk format for resize: {f:?}"),
})
.arg(&self.file_path)
.arg(new_size.as_bytes().to_string());
let output = cmd.output()?;
if !output.status.success() {
anyhow::bail!(
"{} info failed, status: {}, stderr: {}",
constants::PROGRAM_QEMU_IMAGE,
output.status,
String::from_utf8_lossy(&output.stderr)
);
}
Ok(())
}
} }
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
@ -443,7 +327,7 @@ struct QCowInfoOutput {
/// Get QCow2 virtual size /// Get QCow2 virtual size
fn qcow_virt_size(path: &Path) -> anyhow::Result<FileSize> { fn qcow_virt_size(path: &Path) -> anyhow::Result<FileSize> {
// Run qemu-img // Run qemu-img
let mut cmd = Command::new(constants::PROGRAM_QEMU_IMAGE); let mut cmd = Command::new(constants::QEMU_IMAGE_PROGRAM);
cmd.args([ cmd.args([
"info", "info",
path.to_str().unwrap_or(""), path.to_str().unwrap_or(""),
@ -455,7 +339,7 @@ fn qcow_virt_size(path: &Path) -> anyhow::Result<FileSize> {
if !output.status.success() { if !output.status.success() {
anyhow::bail!( anyhow::bail!(
"{} info failed, status: {}, stderr: {}", "{} info failed, status: {}, stderr: {}",
constants::PROGRAM_QEMU_IMAGE, constants::QEMU_IMAGE_PROGRAM,
output.status, output.status,
String::from_utf8_lossy(&output.stderr) String::from_utf8_lossy(&output.stderr)
); );

View File

@ -1,4 +1,3 @@
pub mod cloud_init_utils;
pub mod exec_utils; pub mod exec_utils;
pub mod file_disks_utils; pub mod file_disks_utils;
pub mod file_size_utils; pub mod file_size_utils;

View File

@ -145,13 +145,13 @@ struct IPBridgeInfo {
/// Get the list of bridge interfaces /// Get the list of bridge interfaces
pub fn bridges_list() -> anyhow::Result<Vec<String>> { pub fn bridges_list() -> anyhow::Result<Vec<String>> {
let mut cmd = Command::new(constants::PROGRAM_IP); let mut cmd = Command::new(constants::IP_PROGRAM);
cmd.args(["-json", "link", "show", "type", "bridge"]); cmd.args(["-json", "link", "show", "type", "bridge"]);
let output = cmd.output()?; let output = cmd.output()?;
if !output.status.success() { if !output.status.success() {
anyhow::bail!( anyhow::bail!(
"{} failed, status: {}, stderr: {}", "{} failed, status: {}, stderr: {}",
constants::PROGRAM_IP, constants::IP_PROGRAM,
output.status, output.status,
String::from_utf8_lossy(&output.stderr) String::from_utf8_lossy(&output.stderr)
); );

View File

@ -1,4 +1,3 @@
use chrono::Datelike;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
/// Get the current time since epoch /// Get the current time since epoch
@ -14,15 +13,3 @@ pub fn time() -> u64 {
.unwrap() .unwrap()
.as_secs() .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

@ -13,12 +13,6 @@ enum VMDisksError {
Config(&'static str), Config(&'static str),
} }
#[derive(serde::Serialize, serde::Deserialize)]
pub enum VMDiskBus {
Virtio,
SATA,
}
/// Disk allocation type /// Disk allocation type
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(tag = "format")] #[serde(tag = "format")]
@ -36,23 +30,18 @@ pub struct VMFileDisk {
pub name: String, pub name: String,
/// Disk size, in bytes /// Disk size, in bytes
pub size: FileSize, pub size: FileSize,
/// Disk bus
pub bus: VMDiskBus,
/// Disk format /// Disk format
#[serde(flatten)] #[serde(flatten)]
pub format: VMDiskFormat, pub format: VMDiskFormat,
/// When creating a new disk, specify the disk image template to use /// When creating a new disk, specify the disk image template to use
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub from_image: Option<String>, pub from_image: Option<String>,
/// Set this variable to true to resize disk image
#[serde(skip_serializing_if = "Option::is_none")]
pub resize: Option<bool>,
/// Set this variable to true to delete the disk /// Set this variable to true to delete the disk
pub delete: bool, pub delete: bool,
} }
impl VMFileDisk { impl VMFileDisk {
pub fn load_from_file(path: &str, bus: &str) -> anyhow::Result<Self> { pub fn load_from_file(path: &str) -> anyhow::Result<Self> {
let file = Path::new(path); let file = Path::new(path);
let info = DiskFileInfo::load_file(file)?; let info = DiskFileInfo::load_file(file)?;
@ -72,16 +61,8 @@ impl VMFileDisk {
DiskFileFormat::QCow2 { .. } => VMDiskFormat::QCow2, DiskFileFormat::QCow2 { .. } => VMDiskFormat::QCow2,
_ => anyhow::bail!("Unsupported image format: {:?}", info.format), _ => anyhow::bail!("Unsupported image format: {:?}", info.format),
}, },
bus: match bus {
"virtio" => VMDiskBus::Virtio,
"sata" => VMDiskBus::SATA,
_ => anyhow::bail!("Unsupported disk bus type: {bus}"),
},
delete: false, delete: false,
from_image: None, from_image: None,
resize: None,
}) })
} }
@ -148,40 +129,28 @@ impl VMFileDisk {
if file.exists() { if file.exists() {
log::debug!("File {file:?} does not exists, so it was not touched"); log::debug!("File {file:?} does not exists, so it was not touched");
return Ok(());
} }
// Create disk if required
else {
// Determine file format
let format = match self.format {
VMDiskFormat::Raw { is_sparse } => DiskFileFormat::Raw { is_sparse },
VMDiskFormat::QCow2 => DiskFileFormat::QCow2 {
virtual_size: self.size,
},
};
// Create / Restore disk file let format = match self.format {
match &self.from_image { VMDiskFormat::Raw { is_sparse } => DiskFileFormat::Raw { is_sparse },
// Create disk file VMDiskFormat::QCow2 => DiskFileFormat::QCow2 {
None => { virtual_size: self.size,
DiskFileInfo::create(&file, format, self.size)?; },
} };
// Restore disk image template // Create / Restore disk file
Some(disk_img) => { match &self.from_image {
let src_file = // Create disk file
DiskFileInfo::load_file(&AppConfig::get().disk_images_file_path(disk_img))?; None => {
src_file.convert(&file, format)?; DiskFileInfo::create(&file, format, self.size)?;
}
} }
}
// Resize disk file if requested // Restore disk image template
if self.resize == Some(true) { Some(disk_img) => {
let disk = DiskFileInfo::load_file(&file)?; let src_file =
DiskFileInfo::load_file(&AppConfig::get().disk_images_file_path(disk_img))?;
// Can only increase disk size src_file.convert(&file, format)?;
if let Err(e) = disk.resize(self.size) {
log::error!("Failed to resize disk file {}: {e:?}", self.name);
} }
} }

View File

@ -5,9 +5,9 @@
sudo apt install libvirt-dev sudo apt install libvirt-dev
``` ```
2. Libvirt and cloud image utilities must also be installed: 2. Libvirt must also be installed:
```bash ```bash
sudo apt install qemu-kvm libvirt-daemon-system cloud-image-utils sudo apt install qemu-kvm libvirt-daemon-system
``` ```
3. Allow the current user to manage VMs: 3. Allow the current user to manage VMs:

View File

@ -12,10 +12,10 @@ The release file will be available in `virtweb_backend/target/release/virtweb_ba
This is the only artifact that must be copied to the server. It is recommended to copy it to the `/usr/local/bin` directory. This is the only artifact that must be copied to the server. It is recommended to copy it to the `/usr/local/bin` directory.
## Install requirements ## Install requirements
In order to work properly, VirtWeb relies on `libvirt`, `qemu`, `kvm` and `cloud-localds`: In order to work properly, VirtWeb relies on `libvirt`, `qemu` and `kvm`:
```bash ```bash
sudo apt install qemu-kvm libvirt-daemon-system libvirt0 libvirt-clients libvirt-daemon bridge-utils cloud-image-utils sudo apt install qemu-kvm libvirt-daemon-system libvirt0 libvirt-clients libvirt-daemon bridge-utils
``` ```
## Dedicated user ## Dedicated user

View File

@ -10,34 +10,30 @@
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@fontsource/roboto": "^5.2.6", "@fontsource/roboto": "^5.2.5",
"@mdi/js": "^7.4.47", "@mdi/js": "^7.4.47",
"@mdi/react": "^1.6.1", "@mdi/react": "^1.6.1",
"@monaco-editor/react": "^4.7.0", "@mui/icons-material": "^7.1.0",
"@mui/icons-material": "^7.1.1", "@mui/material": "^7.1.0",
"@mui/material": "^7.1.1",
"@mui/x-charts": "^8.3.1", "@mui/x-charts": "^8.3.1",
"@mui/x-data-grid": "^8.3.1", "@mui/x-data-grid": "^8.3.1",
"date-and-time": "^3.6.0", "date-and-time": "^3.6.0",
"filesize": "^10.1.6", "filesize": "^10.1.6",
"humanize-duration": "^3.32.2", "humanize-duration": "^3.32.2",
"monaco-editor": "^0.52.2",
"monaco-yaml": "^5.4.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-router-dom": "^7.6.2", "react-router-dom": "^7.6.0",
"react-syntax-highlighter": "^15.6.1", "react-syntax-highlighter": "^15.6.1",
"react-vnc": "^3.1.0", "react-vnc": "^3.1.0",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"xml-formatter": "^3.6.6", "xml-formatter": "^3.6.6"
"yaml": "^2.8.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.27.0", "@eslint/js": "^9.27.0",
"@types/humanize-duration": "^3.27.4", "@types/humanize-duration": "^3.27.4",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/react": "^19.1.8", "@types/react": "^19.1.5",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.5",
"@types/react-syntax-highlighter": "^15.5.13", "@types/react-syntax-highlighter": "^15.5.13",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@vitejs/plugin-react": "^4.4.1", "@vitejs/plugin-react": "^4.4.1",
@ -781,9 +777,9 @@
} }
}, },
"node_modules/@fontsource/roboto": { "node_modules/@fontsource/roboto": {
"version": "5.2.6", "version": "5.2.5",
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.2.6.tgz", "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.2.5.tgz",
"integrity": "sha512-hzarG7yAhMoP418smNgfY4fO7UmuUEm5JUtbxCoCcFHT0hOJB+d/qAEyoNjz7YkPU5OjM2LM8rJnW8hfm0JLaA==", "integrity": "sha512-70r2UZ0raqLn5W+sPeKhqlf8wGvUXFWlofaDlcbt/S3d06+17gXKr3VNqDODB0I1ASme3dGT5OJj9NABt7OTZQ==",
"license": "OFL-1.1", "license": "OFL-1.1",
"funding": { "funding": {
"url": "https://github.com/sponsors/ayuhito" "url": "https://github.com/sponsors/ayuhito"
@ -962,33 +958,10 @@
"prop-types": "^15.7.2" "prop-types": "^15.7.2"
} }
}, },
"node_modules/@monaco-editor/loader": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz",
"integrity": "sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==",
"license": "MIT",
"dependencies": {
"state-local": "^1.0.6"
}
},
"node_modules/@monaco-editor/react": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz",
"integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==",
"license": "MIT",
"dependencies": {
"@monaco-editor/loader": "^1.5.0"
},
"peerDependencies": {
"monaco-editor": ">= 0.25.0 < 1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@mui/core-downloads-tracker": { "node_modules/@mui/core-downloads-tracker": {
"version": "7.1.1", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.1.1.tgz", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.1.0.tgz",
"integrity": "sha512-yBckQs4aQ8mqukLnPC6ivIRv6guhaXi8snVl00VtyojBbm+l6VbVhyTSZ68Abcx7Ah8B+GZhrB7BOli+e+9LkQ==", "integrity": "sha512-E0OqhZv548Qdc0PwWhLVA2zmjJZSTvaL4ZhoswmI8NJEC1tpW2js6LLP827jrW9MEiXYdz3QS6+hask83w74yQ==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
@ -996,9 +969,9 @@
} }
}, },
"node_modules/@mui/icons-material": { "node_modules/@mui/icons-material": {
"version": "7.1.1", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.1.1.tgz", "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.1.0.tgz",
"integrity": "sha512-X37+Yc8QpEnl0sYmz+WcLFy2dWgNRzbswDzLPXG7QU1XDVlP5TPp1HXjdmCupOWLL/I9m1fyhcyZl8/HPpp/Cg==", "integrity": "sha512-1mUPMAZ+Qk3jfgL5ftRR06ATH/Esi0izHl1z56H+df6cwIlCWG66RXciUqeJCttbOXOQ5y2DCjLZI/4t3Yg3LA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.27.1" "@babel/runtime": "^7.27.1"
@ -1011,7 +984,7 @@
"url": "https://opencollective.com/mui-org" "url": "https://opencollective.com/mui-org"
}, },
"peerDependencies": { "peerDependencies": {
"@mui/material": "^7.1.1", "@mui/material": "^7.1.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0" "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
}, },
@ -1022,16 +995,16 @@
} }
}, },
"node_modules/@mui/material": { "node_modules/@mui/material": {
"version": "7.1.1", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.1.1.tgz", "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.1.0.tgz",
"integrity": "sha512-mTpdmdZCaHCGOH3SrYM41+XKvNL0iQfM9KlYgpSjgadXx/fEKhhvOktxm8++Xw6FFeOHoOiV+lzOI8X1rsv71A==", "integrity": "sha512-ahUJdrhEv+mCp4XHW+tHIEYzZMSRLg8z4AjUOsj44QpD1ZaMxQoVOG2xiHvLFdcsIPbgSRx1bg1eQSheHBgvtg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.27.1", "@babel/runtime": "^7.27.1",
"@mui/core-downloads-tracker": "^7.1.1", "@mui/core-downloads-tracker": "^7.1.0",
"@mui/system": "^7.1.1", "@mui/system": "^7.1.0",
"@mui/types": "^7.4.3", "@mui/types": "^7.4.2",
"@mui/utils": "^7.1.1", "@mui/utils": "^7.1.0",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@types/react-transition-group": "^4.4.12", "@types/react-transition-group": "^4.4.12",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@ -1050,7 +1023,7 @@
"peerDependencies": { "peerDependencies": {
"@emotion/react": "^11.5.0", "@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0", "@emotion/styled": "^11.3.0",
"@mui/material-pigment-css": "^7.1.1", "@mui/material-pigment-css": "^7.1.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
@ -1071,13 +1044,13 @@
} }
}, },
"node_modules/@mui/private-theming": { "node_modules/@mui/private-theming": {
"version": "7.1.1", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.1.tgz", "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.0.tgz",
"integrity": "sha512-M8NbLUx+armk2ZuaxBkkMk11ultnWmrPlN0Xe3jUEaBChg/mcxa5HWIWS1EE4DF36WRACaAHVAvyekWlDQf0PQ==", "integrity": "sha512-4Kck4jxhqF6YxNwJdSae1WgDfXVg0lIH6JVJ7gtuFfuKcQCgomJxPvUEOySTFRPz1IZzwz5OAcToskRdffElDA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.27.1", "@babel/runtime": "^7.27.1",
"@mui/utils": "^7.1.1", "@mui/utils": "^7.1.0",
"prop-types": "^15.8.1" "prop-types": "^15.8.1"
}, },
"engines": { "engines": {
@ -1098,9 +1071,9 @@
} }
}, },
"node_modules/@mui/styled-engine": { "node_modules/@mui/styled-engine": {
"version": "7.1.1", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.1.1.tgz", "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.1.0.tgz",
"integrity": "sha512-R2wpzmSN127j26HrCPYVQ53vvMcT5DaKLoWkrfwUYq3cYytL6TQrCH8JBH3z79B6g4nMZZVoaXrxO757AlShaw==", "integrity": "sha512-m0mJ0c6iRC+f9hMeRe0W7zZX1wme3oUX0+XTVHjPG7DJz6OdQ6K/ggEOq7ZdwilcpdsDUwwMfOmvO71qDkYd2w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.27.1", "@babel/runtime": "^7.27.1",
@ -1132,16 +1105,16 @@
} }
}, },
"node_modules/@mui/system": { "node_modules/@mui/system": {
"version": "7.1.1", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-7.1.1.tgz", "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.1.0.tgz",
"integrity": "sha512-Kj1uhiqnj4Zo7PDjAOghtXJtNABunWvhcRU0O7RQJ7WOxeynoH6wXPcilphV8QTFtkKaip8EiNJRiCD+B3eROA==", "integrity": "sha512-iedAWgRJMCxeMHvkEhsDlbvkK+qKf9me6ofsf7twk/jfT4P1ImVf7Rwb5VubEA0sikrVL+1SkoZM41M4+LNAVA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.27.1", "@babel/runtime": "^7.27.1",
"@mui/private-theming": "^7.1.1", "@mui/private-theming": "^7.1.0",
"@mui/styled-engine": "^7.1.1", "@mui/styled-engine": "^7.1.0",
"@mui/types": "^7.4.3", "@mui/types": "^7.4.2",
"@mui/utils": "^7.1.1", "@mui/utils": "^7.1.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"csstype": "^3.1.3", "csstype": "^3.1.3",
"prop-types": "^15.8.1" "prop-types": "^15.8.1"
@ -1172,9 +1145,9 @@
} }
}, },
"node_modules/@mui/types": { "node_modules/@mui/types": {
"version": "7.4.3", "version": "7.4.2",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.3.tgz", "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.2.tgz",
"integrity": "sha512-2UCEiK29vtiZTeLdS2d4GndBKacVyxGvReznGXGr+CzW/YhjIX+OHUdCIczZjzcRAgKBGmE9zCIgoV9FleuyRQ==", "integrity": "sha512-edRc5JcLPsrlNFYyTPxds+d5oUovuUxnnDtpJUbP6WMeV4+6eaX/mqai1ZIWT62lCOe0nlrON0s9HDiv5en5bA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.27.1" "@babel/runtime": "^7.27.1"
@ -1189,13 +1162,13 @@
} }
}, },
"node_modules/@mui/utils": { "node_modules/@mui/utils": {
"version": "7.1.1", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.1.tgz", "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz",
"integrity": "sha512-BkOt2q7MBYl7pweY2JWwfrlahhp+uGLR8S+EhiyRaofeRYUWL2YKbSGQvN4hgSN1i8poN0PaUiii1kEMrchvzg==", "integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.27.1", "@babel/runtime": "^7.27.1",
"@mui/types": "^7.4.3", "@mui/types": "^7.4.2",
"@types/prop-types": "^15.7.14", "@types/prop-types": "^15.7.14",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
@ -1625,18 +1598,18 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "19.1.8", "version": "19.1.5",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.5.tgz",
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", "integrity": "sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
}, },
"node_modules/@types/react-dom": { "node_modules/@types/react-dom": {
"version": "19.1.6", "version": "19.1.5",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.5.tgz",
"integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", "integrity": "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
@ -3508,12 +3481,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/jsonc-parser": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
"integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
"license": "MIT"
},
"node_modules/keyv": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -3640,84 +3607,6 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/monaco-editor": {
"version": "0.52.2",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz",
"integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==",
"license": "MIT"
},
"node_modules/monaco-languageserver-types": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/monaco-languageserver-types/-/monaco-languageserver-types-0.4.0.tgz",
"integrity": "sha512-QQ3BZiU5LYkJElGncSNb5AKoJ/LCs6YBMCJMAz9EA7v+JaOdn3kx2cXpPTcZfKA5AEsR0vc97sAw+5mdNhVBmw==",
"license": "MIT",
"dependencies": {
"monaco-types": "^0.1.0",
"vscode-languageserver-protocol": "^3.0.0",
"vscode-uri": "^3.0.0"
},
"funding": {
"url": "https://github.com/sponsors/remcohaszing"
}
},
"node_modules/monaco-marker-data-provider": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/monaco-marker-data-provider/-/monaco-marker-data-provider-1.2.4.tgz",
"integrity": "sha512-4DsPgsAqpTyUDs3humXRBPUJoihTv+L6v9aupQWD80X2YXaCXUd11mWYeSCYHuPgdUmjFaNWCEOjQ6ewf/QA1Q==",
"license": "MIT",
"dependencies": {
"monaco-types": "^0.1.0"
},
"funding": {
"url": "https://github.com/sponsors/remcohaszing"
}
},
"node_modules/monaco-types": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/monaco-types/-/monaco-types-0.1.0.tgz",
"integrity": "sha512-aWK7SN9hAqNYi0WosPoMjenMeXJjwCxDibOqWffyQ/qXdzB/86xshGQobRferfmNz7BSNQ8GB0MD0oby9/5fTQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/remcohaszing"
}
},
"node_modules/monaco-worker-manager": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/monaco-worker-manager/-/monaco-worker-manager-2.0.1.tgz",
"integrity": "sha512-kdPL0yvg5qjhKPNVjJoym331PY/5JC11aPJXtCZNwWRvBr6jhkIamvYAyiY5P1AWFmNOy0aRDRoMdZfa71h8kg==",
"license": "MIT",
"peerDependencies": {
"monaco-editor": ">=0.30.0"
}
},
"node_modules/monaco-yaml": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/monaco-yaml/-/monaco-yaml-5.4.0.tgz",
"integrity": "sha512-tuBVDy1KAPrgO905GHTItu8AaA5bIzF5S4X0JVRAE/D66FpRhkDUk7tKi5bwKMVTTugtpMLsXN4ewh4CgE/FtQ==",
"license": "MIT",
"workspaces": [
"examples/*"
],
"dependencies": {
"jsonc-parser": "^3.0.0",
"monaco-languageserver-types": "^0.4.0",
"monaco-marker-data-provider": "^1.0.0",
"monaco-types": "^0.1.0",
"monaco-worker-manager": "^2.0.0",
"path-browserify": "^1.0.0",
"prettier": "^3.0.0",
"vscode-languageserver-textdocument": "^1.0.0",
"vscode-languageserver-types": "^3.0.0",
"vscode-uri": "^3.0.0",
"yaml": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/remcohaszing"
},
"peerDependencies": {
"monaco-editor": ">=0.36"
}
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -3864,12 +3753,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
"license": "MIT"
},
"node_modules/path-exists": { "node_modules/path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -3963,21 +3846,6 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/prettier": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/pretty-format": { "node_modules/pretty-format": {
"version": "29.7.0", "version": "29.7.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
@ -4121,9 +3989,9 @@
} }
}, },
"node_modules/react-router": { "node_modules/react-router": {
"version": "7.6.2", "version": "7.6.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.2.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.0.tgz",
"integrity": "sha512-U7Nv3y+bMimgWjhlT5CRdzHPu2/KVmqPwKUCChW8en5P3znxUqwlYFlbmyj8Rgp1SF6zs5X4+77kBVknkg6a0w==", "integrity": "sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cookie": "^1.0.1", "cookie": "^1.0.1",
@ -4143,12 +4011,12 @@
} }
}, },
"node_modules/react-router-dom": { "node_modules/react-router-dom": {
"version": "7.6.2", "version": "7.6.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.2.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.0.tgz",
"integrity": "sha512-Q8zb6VlTbdYKK5JJBLQEN06oTUa/RAbG/oQS1auK1I0TbJOXktqm+QENEVJU6QvWynlXPRBXI3fiOQcSEA78rA==", "integrity": "sha512-DYgm6RDEuKdopSyGOWZGtDfSm7Aofb8CCzgkliTjtu/eDuB0gcsv6qdFhhi8HdtmA+KHkt5MfZ5K2PdzjugYsA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"react-router": "7.6.2" "react-router": "7.6.0"
}, },
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"
@ -4458,12 +4326,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/state-local": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
"integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
"license": "MIT"
},
"node_modules/string-ts": { "node_modules/string-ts": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/string-ts/-/string-ts-2.2.1.tgz", "resolved": "https://registry.npmjs.org/string-ts/-/string-ts-2.2.1.tgz",
@ -4852,43 +4714,6 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/vscode-jsonrpc": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
"integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/vscode-languageserver-protocol": {
"version": "3.17.5",
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
"integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
"license": "MIT",
"dependencies": {
"vscode-jsonrpc": "8.2.0",
"vscode-languageserver-types": "3.17.5"
}
},
"node_modules/vscode-languageserver-textdocument": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz",
"integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==",
"license": "MIT"
},
"node_modules/vscode-languageserver-types": {
"version": "3.17.5",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
"integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
"license": "MIT"
},
"node_modules/vscode-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
"integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
"license": "MIT"
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -4956,7 +4781,10 @@
"version": "2.8.0", "version": "2.8.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
"integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
"dev": true,
"license": "ISC", "license": "ISC",
"optional": true,
"peer": true,
"bin": { "bin": {
"yaml": "bin.mjs" "yaml": "bin.mjs"
}, },

View File

@ -12,34 +12,30 @@
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@fontsource/roboto": "^5.2.6", "@fontsource/roboto": "^5.2.5",
"@mdi/js": "^7.4.47", "@mdi/js": "^7.4.47",
"@mdi/react": "^1.6.1", "@mdi/react": "^1.6.1",
"@monaco-editor/react": "^4.7.0", "@mui/icons-material": "^7.1.0",
"@mui/icons-material": "^7.1.1", "@mui/material": "^7.1.0",
"@mui/material": "^7.1.1",
"@mui/x-charts": "^8.3.1", "@mui/x-charts": "^8.3.1",
"@mui/x-data-grid": "^8.3.1", "@mui/x-data-grid": "^8.3.1",
"date-and-time": "^3.6.0", "date-and-time": "^3.6.0",
"filesize": "^10.1.6", "filesize": "^10.1.6",
"humanize-duration": "^3.32.2", "humanize-duration": "^3.32.2",
"monaco-editor": "^0.52.2",
"monaco-yaml": "^5.4.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-router-dom": "^7.6.2", "react-router-dom": "^7.6.0",
"react-syntax-highlighter": "^15.6.1", "react-syntax-highlighter": "^15.6.1",
"react-vnc": "^3.1.0", "react-vnc": "^3.1.0",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"xml-formatter": "^3.6.6", "xml-formatter": "^3.6.6"
"yaml": "^2.8.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.27.0", "@eslint/js": "^9.27.0",
"@types/humanize-duration": "^3.27.4", "@types/humanize-duration": "^3.27.4",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/react": "^19.1.8", "@types/react": "^19.1.5",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.5",
"@types/react-syntax-highlighter": "^15.5.13", "@types/react-syntax-highlighter": "^15.5.13",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@vitejs/plugin-react": "^4.4.1", "@vitejs/plugin-react": "^4.4.1",

View File

@ -4,10 +4,8 @@ import { VMFileDisk, VMInfo } from "./VMApi";
export type DiskImageFormat = export type DiskImageFormat =
| { format: "Raw"; is_sparse: boolean } | { format: "Raw"; is_sparse: boolean }
| { format: "QCow2"; virtual_size?: number } | { format: "QCow2"; virtual_size?: number }
| { format: "GzCompressedQCow2" } | { format: "CompressedQCow2" }
| { format: "GzCompressedRaw" } | { format: "CompressedRaw" };
| { format: "XzCompressedQCow2" }
| { format: "XzCompressedRaw" };
export type DiskImage = { export type DiskImage = {
file_size: number; file_size: number;
@ -96,17 +94,6 @@ export class DiskImageApi {
}); });
} }
/**
* Rename disk image file
*/
static async Rename(file: DiskImage, name: string): Promise<void> {
await APIClient.exec({
method: "POST",
uri: `/disk_images/${file.file_name}/rename`,
jsonData: { name },
});
}
/** /**
* Delete disk image file * Delete disk image file
*/ */

View File

@ -232,16 +232,4 @@ export class ServerApi {
}) })
).data; ).data;
} }
/**
* Export all server configs
*/
static async ExportServerConfigs(): Promise<Blob> {
return (
await APIClient.exec({
method: "GET",
uri: "/server/export_configs",
})
).data;
}
} }

View File

@ -19,24 +19,16 @@ export type VMState =
export type VMFileDisk = BaseFileVMDisk & (RawVMDisk | QCow2Disk); export type VMFileDisk = BaseFileVMDisk & (RawVMDisk | QCow2Disk);
export type DiskBusType = "Virtio" | "SATA";
export interface BaseFileVMDisk { export interface BaseFileVMDisk {
size: number; size: number;
name: string; name: string;
bus: DiskBusType;
delete: boolean; delete: boolean;
// For new disk only // For new disk only
from_image?: string; from_image?: string;
// Resize disk image after clone
resize?: boolean;
// application attributes // application attributes
new?: boolean; new?: boolean;
originalSize?: number;
deleteType?: "keepfile" | "deletefile"; deleteType?: "keepfile" | "deletefile";
} }
@ -86,17 +78,6 @@ export interface VMNetBridge {
bridge: string; bridge: string;
} }
export interface VMCloudInit {
attach_config: boolean;
user_data: string;
instance_id?: string;
local_hostname?: string;
dsmode?: "Net" | "Local";
network_configuration?: string;
}
export type VMBootType = "UEFI" | "UEFISecureBoot" | "Legacy";
interface VMInfoInterface { interface VMInfoInterface {
name: string; name: string;
uuid?: string; uuid?: string;
@ -104,7 +85,7 @@ interface VMInfoInterface {
title?: string; title?: string;
description?: string; description?: string;
group?: string; group?: string;
boot_type: VMBootType; boot_type: "UEFI" | "UEFISecureBoot";
architecture: "i686" | "x86_64"; architecture: "i686" | "x86_64";
memory: number; memory: number;
number_vcpu: number; number_vcpu: number;
@ -114,7 +95,6 @@ interface VMInfoInterface {
networks: VMNetInterface[]; networks: VMNetInterface[];
tpm_module: boolean; tpm_module: boolean;
oem_strings: string[]; oem_strings: string[];
cloud_init: VMCloudInit;
} }
export class VMInfo implements VMInfoInterface { export class VMInfo implements VMInfoInterface {
@ -124,7 +104,7 @@ export class VMInfo implements VMInfoInterface {
title?: string; title?: string;
description?: string; description?: string;
group?: string; group?: string;
boot_type: VMBootType; boot_type: "UEFI" | "UEFISecureBoot";
architecture: "i686" | "x86_64"; architecture: "i686" | "x86_64";
number_vcpu: number; number_vcpu: number;
memory: number; memory: number;
@ -134,7 +114,6 @@ export class VMInfo implements VMInfoInterface {
networks: VMNetInterface[]; networks: VMNetInterface[];
tpm_module: boolean; tpm_module: boolean;
oem_strings: string[]; oem_strings: string[];
cloud_init: VMCloudInit;
constructor(int: VMInfoInterface) { constructor(int: VMInfoInterface) {
this.name = int.name; this.name = int.name;
@ -153,7 +132,6 @@ export class VMInfo implements VMInfoInterface {
this.networks = int.networks; this.networks = int.networks;
this.tpm_module = int.tpm_module; this.tpm_module = int.tpm_module;
this.oem_strings = int.oem_strings; this.oem_strings = int.oem_strings;
this.cloud_init = int.cloud_init;
} }
static NewEmpty(): VMInfo { static NewEmpty(): VMInfo {
@ -169,7 +147,6 @@ export class VMInfo implements VMInfoInterface {
networks: [], networks: [],
tpm_module: true, tpm_module: true,
oem_strings: [], oem_strings: [],
cloud_init: { attach_config: false, user_data: "" },
}); });
} }

View File

@ -42,15 +42,13 @@ export function ConvertDiskImageDialog(
setFormat({ format: value ?? ("QCow2" as any) }); setFormat({ format: value ?? ("QCow2" as any) });
if (value === "QCow2") setFilename(`${origFilename}.qcow2`); if (value === "QCow2") setFilename(`${origFilename}.qcow2`);
if (value === "GzCompressedQCow2") setFilename(`${origFilename}.qcow2.gz`); if (value === "CompressedQCow2") setFilename(`${origFilename}.qcow2.gz`);
if (value === "XzCompressedQCow2") setFilename(`${origFilename}.qcow2.xz`);
if (value === "Raw") { if (value === "Raw") {
setFilename(`${origFilename}.raw`); setFilename(`${origFilename}.raw`);
// Check sparse checkbox by default // Check sparse checkbox by default
setFormat({ format: "Raw", is_sparse: true }); setFormat({ format: "Raw", is_sparse: true });
} }
if (value === "GzCompressedRaw") setFilename(`${origFilename}.raw.gz`); if (value === "CompressedRaw") setFilename(`${origFilename}.raw.gz`);
if (value === "XzCompressedRaw") setFilename(`${origFilename}.raw.xz`);
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
@ -106,10 +104,8 @@ export function ConvertDiskImageDialog(
options={[ options={[
{ value: "QCow2" }, { value: "QCow2" },
{ value: "Raw" }, { value: "Raw" },
{ value: "GzCompressedRaw" }, { value: "CompressedRaw" },
{ value: "XzCompressedRaw" }, { value: "CompressedQCow2" },
{ value: "GzCompressedQCow2" },
{ value: "XzCompressedQCow2" },
]} ]}
/> />

View File

@ -3,44 +3,16 @@ import "@fontsource/roboto/400.css";
import "@fontsource/roboto/500.css"; import "@fontsource/roboto/500.css";
import "@fontsource/roboto/700.css"; import "@fontsource/roboto/700.css";
import { ThemeProvider, createTheme } from "@mui/material";
import React from "react"; import React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import { App } from "./App"; import { App } from "./App";
import { AlertDialogProvider } from "./hooks/providers/AlertDialogProvider";
import { ConfirmDialogProvider } from "./hooks/providers/ConfirmDialogProvider";
import { LoadingMessageProvider } from "./hooks/providers/LoadingMessageProvider";
import { SnackbarProvider } from "./hooks/providers/SnackbarProvider";
import "./index.css"; import "./index.css";
import { LoadServerConfig } from "./widgets/LoadServerConfig"; import { LoadServerConfig } from "./widgets/LoadServerConfig";
import { ThemeProvider, createTheme } from "@mui/material";
import { loader } from "@monaco-editor/react"; import { LoadingMessageProvider } from "./hooks/providers/LoadingMessageProvider";
import * as monaco from "monaco-editor"; import { AlertDialogProvider } from "./hooks/providers/AlertDialogProvider";
import EditorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker"; import { SnackbarProvider } from "./hooks/providers/SnackbarProvider";
import { configureMonacoYaml } from "monaco-yaml"; import { ConfirmDialogProvider } from "./hooks/providers/ConfirmDialogProvider";
import YamlWorker from "monaco-yaml/yaml.worker?worker";
// This allows to use a self hosted instance of Monaco editor
loader.config({ monaco });
// Add YAML support to Monaco
configureMonacoYaml(monaco, {
enableSchemaRequest: false,
});
/// YAML worker
window.MonacoEnvironment = {
getWorker(_moduleId, label) {
switch (label) {
case "editorWorkerService":
return new EditorWorker();
case "yaml":
return new YamlWorker();
default:
throw new Error(`Unknown label ${label}`);
}
},
};
const darkTheme = createTheme({ const darkTheme = createTheme({
palette: { palette: {
@ -48,7 +20,9 @@ const darkTheme = createTheme({
}, },
}); });
const root = ReactDOM.createRoot(document.getElementById("root")!); const root = ReactDOM.createRoot(
document.getElementById("root")!
);
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<ThemeProvider theme={darkTheme}> <ThemeProvider theme={darkTheme}>

View File

@ -1,7 +1,6 @@
import DeleteIcon from "@mui/icons-material/Delete"; import DeleteIcon from "@mui/icons-material/Delete";
import DownloadIcon from "@mui/icons-material/Download"; import DownloadIcon from "@mui/icons-material/Download";
import LoopIcon from "@mui/icons-material/Loop"; import LoopIcon from "@mui/icons-material/Loop";
import MoreVertIcon from "@mui/icons-material/MoreVert";
import RefreshIcon from "@mui/icons-material/Refresh"; import RefreshIcon from "@mui/icons-material/Refresh";
import { import {
Alert, Alert,
@ -9,10 +8,6 @@ import {
CircularProgress, CircularProgress,
IconButton, IconButton,
LinearProgress, LinearProgress,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Tooltip, Tooltip,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
@ -169,11 +164,15 @@ function DiskImageList(p: {
const confirm = useConfirm(); const confirm = useConfirm();
const loadingMessage = useLoadingMessage(); const loadingMessage = useLoadingMessage();
const [dlProgress, setDlProgress] = React.useState<undefined | number>();
const [currConversion, setCurrConversion] = React.useState< const [currConversion, setCurrConversion] = React.useState<
DiskImage | undefined DiskImage | undefined
>(); >();
const [dlProgress, setDlProgress] = React.useState<undefined | number>();
// Convert disk image file
const convertDiskImage = (entry: DiskImage) => {
setCurrConversion(entry);
};
// Download disk image file // Download disk image file
const downloadDiskImage = async (entry: DiskImage) => { const downloadDiskImage = async (entry: DiskImage) => {
@ -191,11 +190,6 @@ function DiskImageList(p: {
setDlProgress(undefined); setDlProgress(undefined);
}; };
// Convert disk image file
const convertDiskImage = (entry: DiskImage) => {
setCurrConversion(entry);
};
// Delete disk image // Delete disk image
const deleteDiskImage = async (entry: DiskImage) => { const deleteDiskImage = async (entry: DiskImage) => {
if ( if (
@ -227,7 +221,7 @@ function DiskImageList(p: {
); );
const columns: GridColDef<(typeof p.list)[number]>[] = [ const columns: GridColDef<(typeof p.list)[number]>[] = [
{ field: "file_name", headerName: "File name", flex: 3, editable: true }, { field: "file_name", headerName: "File name", flex: 3 },
{ {
field: "format", field: "format",
headerName: "Format", headerName: "Format",
@ -266,21 +260,28 @@ function DiskImageList(p: {
}, },
{ {
field: "actions", field: "actions",
type: "actions",
headerName: "", headerName: "",
width: 55, width: 140,
cellClassName: "actions", renderCell(params) {
editable: false, return (
getActions: (params) => { <>
return [ <Tooltip title="Convert disk image">
<DiskImageActionMenu <IconButton onClick={() => { convertDiskImage(params.row); }}>
key="menu" <LoopIcon />
diskImage={params.row} </IconButton>
onDownload={downloadDiskImage} </Tooltip>
onConvert={convertDiskImage} <Tooltip title="Download disk image">
onDelete={deleteDiskImage} <IconButton onClick={() => downloadDiskImage(params.row)}>
/>, <DownloadIcon />
]; </IconButton>
</Tooltip>
<Tooltip title="Delete disk image">
<IconButton onClick={() => deleteDiskImage(params.row)}>
<DeleteIcon />
</IconButton>
</Tooltip>
</>
);
}, },
}, },
]; ];
@ -326,92 +327,7 @@ function DiskImageList(p: {
)} )}
{/* The table itself */} {/* The table itself */}
<DataGrid<DiskImage> <DataGrid getRowId={(c) => c.file_name} rows={p.list} columns={columns} />
getRowId={(c) => c.file_name}
rows={p.list}
columns={columns}
processRowUpdate={async (n, o) => {
try {
await DiskImageApi.Rename(o, n.file_name);
return n;
} catch (e) {
console.error("Failed to rename disk image!", e);
alert(`Failed to rename disk image! ${e}`);
throw e;
} finally {
p.onReload();
}
}}
/>
</>
);
}
function DiskImageActionMenu(p: {
diskImage: DiskImage;
onDownload: (d: DiskImage) => void;
onConvert: (d: DiskImage) => void;
onDelete: (d: DiskImage) => void;
}): React.ReactElement {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
return (
<>
<IconButton
aria-label="Actions"
aria-haspopup="true"
onClick={handleClick}
>
<MoreVertIcon />
</IconButton>
<Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
{/* Download disk image */}
<MenuItem
onClick={() => {
handleClose();
p.onDownload(p.diskImage);
}}
>
<ListItemIcon>
<DownloadIcon />
</ListItemIcon>
<ListItemText secondary={"Download disk image"}>
Download
</ListItemText>
</MenuItem>
{/* Convert disk image */}
<MenuItem
onClick={() => {
handleClose();
p.onConvert(p.diskImage);
}}
>
<ListItemIcon>
<LoopIcon />
</ListItemIcon>
<ListItemText secondary={"Convert disk image"}>Convert</ListItemText>
</MenuItem>
{/* Delete disk image */}
<MenuItem
onClick={() => {
handleClose();
p.onDelete(p.diskImage);
}}
>
<ListItemIcon>
<DeleteIcon color="error" />
</ListItemIcon>
<ListItemText secondary={"Delete disk image"}>Delete</ListItemText>
</MenuItem>
</Menu>
</> </>
); );
} }

View File

@ -9,21 +9,18 @@ import {
import Icon from "@mdi/react"; import Icon from "@mdi/react";
import { import {
Box, Box,
IconButton,
LinearProgress, LinearProgress,
Table, Table,
TableBody, TableBody,
TableCell, TableCell,
TableHead, TableHead,
TableRow, TableRow,
Tooltip,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import { PieChart } from "@mui/x-charts"; import { PieChart } from "@mui/x-charts";
import { filesize } from "filesize"; import { filesize } from "filesize";
import humanizeDuration from "humanize-duration"; import humanizeDuration from "humanize-duration";
import IosShareIcon from "@mui/icons-material/IosShare";
import React from "react"; import React from "react";
import { import {
DiskInfo, DiskInfo,
@ -34,8 +31,6 @@ import {
import { AsyncWidget } from "../widgets/AsyncWidget"; import { AsyncWidget } from "../widgets/AsyncWidget";
import { VirtWebPaper } from "../widgets/VirtWebPaper"; import { VirtWebPaper } from "../widgets/VirtWebPaper";
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider";
import { useAlert } from "../hooks/providers/AlertDialogProvider";
export function SysInfoRoute(): React.ReactElement { export function SysInfoRoute(): React.ReactElement {
const [info, setInfo] = React.useState<ServerSystemInfo>(); const [info, setInfo] = React.useState<ServerSystemInfo>();
@ -57,23 +52,6 @@ export function SysInfoRoute(): React.ReactElement {
export function SysInfoRouteInner(p: { export function SysInfoRouteInner(p: {
info: ServerSystemInfo; info: ServerSystemInfo;
}): React.ReactElement { }): 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( const sumDiskUsage = p.info.disks.reduce(
(prev, disk) => { (prev, disk) => {
return { return {
@ -85,16 +63,7 @@ export function SysInfoRouteInner(p: {
); );
return ( return (
<VirtWebRouteContainer <VirtWebRouteContainer label="Sysinfo">
label="Sysinfo"
actions={
<Tooltip title="Export all server configs">
<IconButton onClick={downloadAllConfig}>
<IosShareIcon />
</IconButton>
</Tooltip>
}
>
<Grid container spacing={2}> <Grid container spacing={2}>
{/* Memory */} {/* Memory */}
<Grid size={{ xs: 4 }}> <Grid size={{ xs: 4 }}>

View File

@ -1,5 +1,5 @@
import VisibilityIcon from "@mui/icons-material/Visibility"; import VisibilityIcon from '@mui/icons-material/Visibility';
import VisibilityOffIcon from "@mui/icons-material/VisibilityOff"; import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import { import {
Alert, Alert,
CircularProgress, CircularProgress,
@ -36,9 +36,7 @@ export function LoginRoute(): React.ReactElement {
const canSubmit = username.length > 0 && password.length > 0; const canSubmit = username.length > 0 && password.length > 0;
const [showPassword, setShowPassword] = React.useState(false); const [showPassword, setShowPassword] = React.useState(false);
const handleClickShowPassword = () => { const handleClickShowPassword = () => { setShowPassword((show) => !show); };
setShowPassword((show) => !show);
};
const handleMouseDownPassword = ( const handleMouseDownPassword = (
event: React.MouseEvent<HTMLButtonElement> event: React.MouseEvent<HTMLButtonElement>
@ -107,14 +105,12 @@ export function LoginRoute(): React.ReactElement {
label="Username" label="Username"
name="username" name="username"
value={username} value={username}
onChange={(e) => { onChange={(e) => { setUsername(e.target.value); }}
setUsername(e.target.value);
}}
autoComplete="username" autoComplete="username"
autoFocus autoFocus
/> />
<FormControl required fullWidth variant="outlined"> <FormControl fullWidth variant="outlined">
<InputLabel htmlFor="password">Password</InputLabel> <InputLabel htmlFor="password">Password</InputLabel>
<OutlinedInput <OutlinedInput
required required
@ -124,9 +120,7 @@ export function LoginRoute(): React.ReactElement {
type={showPassword ? "text" : "password"} type={showPassword ? "text" : "password"}
id="password" id="password"
value={password} value={password}
onChange={(e) => { onChange={(e) => { setPassword(e.target.value); }}
setPassword(e.target.value);
}}
autoComplete="current-password" autoComplete="current-password"
endAdornment={ endAdornment={
<InputAdornment position="end"> <InputAdornment position="end">
@ -137,11 +131,7 @@ export function LoginRoute(): React.ReactElement {
onMouseDown={handleMouseDownPassword} onMouseDown={handleMouseDownPassword}
edge="end" edge="end"
> >
{showPassword ? ( {showPassword ? <VisibilityOffIcon /> : <VisibilityIcon />}
<VisibilityOffIcon />
) : (
<VisibilityIcon />
)}
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</InputAdornment> </InputAdornment>

View File

@ -17,9 +17,7 @@ export function CheckboxInput(p: {
<Checkbox <Checkbox
disabled={!p.editable} disabled={!p.editable}
checked={p.checked} checked={p.checked}
onChange={(e) => { onChange={(e) => { p.onValueChange(e.target.checked); }}
p.onValueChange(e.target.checked);
}}
/> />
} }
label={p.label} label={p.label}

View File

@ -1,341 +0,0 @@
/* 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";
import { TextInput } from "./TextInput";
interface CloudInitProps {
vm: VMInfo;
onChange?: () => void;
editable: boolean;
}
export function CloudInitEditor(p: CloudInitProps): React.ReactElement {
return (
<>
<EditSection>
{/* Attach cloud init disk */}
<CheckboxInput
{...p}
label="Attach Cloud Init disk"
checked={p.vm.cloud_init.attach_config}
onValueChange={(v) => {
p.vm.cloud_init.attach_config = v;
p.onChange?.();
}}
/>
</EditSection>
<Grid container spacing={2}>
<CloudInitMetadata
{...p}
editable={p.editable && p.vm.cloud_init.attach_config}
/>
<CloudInitRawUserData
{...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>
</>
);
}
function CloudInitMetadata(p: CloudInitProps): React.ReactElement {
// Regenerate instance id
const reGenerateInstanceId = () => {
p.vm.cloud_init.instance_id = uuidv4();
p.onChange?.();
};
return (
<EditSection title="Metadata">
{/* Instance ID */}
<TextInput
{...p}
label="Instance ID"
value={p.vm.cloud_init.instance_id}
onValueChange={(v) => {
p.vm.cloud_init.instance_id = v;
p.onChange?.();
}}
endAdornment={
p.editable ? (
<InputAdornment position="end">
<Tooltip title="Generate a new instance ID">
<IconButton onClick={reGenerateInstanceId}>
<RefreshIcon />
</IconButton>
</Tooltip>
</InputAdornment>
) : (
<></>
)
}
/>
{/* Instance hostname */}
<TextInput
{...p}
label="Local hostname"
value={p.vm.cloud_init.local_hostname}
onValueChange={(v) => {
p.vm.cloud_init.local_hostname = v;
p.onChange?.();
}}
/>
{/* Data source mode */}
<SelectInput
{...p}
label="Data source mode"
value={p.vm.cloud_init.dsmode}
onValueChange={(v) => {
p.vm.cloud_init.dsmode = v as any;
p.onChange?.();
}}
options={[
{ label: "None", value: undefined },
{ value: "Net" },
{ value: "Local" },
]}
/>
</EditSection>
);
}
function CloudInitRawUserData(p: CloudInitProps): React.ReactElement {
return (
<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"}
value={p.vm.cloud_init.user_data}
onChange={(v) => {
p.vm.cloud_init.user_data = v ?? "";
p.onChange?.();
}}
/>
</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

@ -1,24 +0,0 @@
import { DiskBusType } from "../../api/VMApi";
import { SelectInput } from "./SelectInput";
export function DiskBusSelect(p: {
editable: boolean;
value: DiskBusType;
label?: string;
onValueChange: (value: DiskBusType) => void;
size?: "medium" | "small";
disableUnderline?: boolean;
disableBottomMargin?: boolean;
}): React.ReactElement {
return (
<SelectInput
{...p}
label={p.label ?? "Disk bus type"}
options={[
{ label: "virtio", value: "Virtio" },
{ label: "sata", value: "SATA" },
]}
onValueChange={(v) => { p.onValueChange(v as any); }}
/>
);
}

View File

@ -1,12 +1,12 @@
import React from "react";
import { DiskImage } from "../../api/DiskImageApi";
import { import {
FormControl, FormControl,
InputLabel, InputLabel,
MenuItem,
Select, Select,
MenuItem,
SelectChangeEvent, SelectChangeEvent,
} from "@mui/material"; } from "@mui/material";
import React from "react";
import { DiskImage } from "../../api/DiskImageApi";
import { FileDiskImageWidget } from "../FileDiskImageWidget"; import { FileDiskImageWidget } from "../FileDiskImageWidget";
/** /**
@ -30,7 +30,7 @@ export function DiskImageSelect(p: {
<i>None</i> <i>None</i>
</MenuItem> </MenuItem>
{p.list.map((d) => ( {p.list.map((d) => (
<MenuItem key={d.file_name} value={d.file_name}> <MenuItem value={d.file_name}>
<FileDiskImageWidget image={d} /> <FileDiskImageWidget image={d} />
</MenuItem> </MenuItem>
))} ))}

View File

@ -1,25 +0,0 @@
import { ServerApi } from "../../api/ServerApi";
import { TextInput } from "./TextInput";
export function DiskSizeInput(p: {
editable: boolean;
label?: string;
value: number;
onChange?: (size: number) => void;
}): React.ReactElement {
return (
<TextInput
editable={p.editable}
label={p.label ?? "Disk size (GB)"}
size={{
min: ServerApi.Config.constraints.disk_size.min / (1000 * 1000 * 1000),
max: ServerApi.Config.constraints.disk_size.max / (1000 * 1000 * 1000),
}}
value={(p.value / (1000 * 1000 * 1000)).toString()}
onValueChange={(v) => {
p.onChange?.(Number(v ?? "0") * 1000 * 1000 * 1000);
}}
type="number"
/>
);
}

View File

@ -19,10 +19,13 @@ export function EditSection(
display: "flex", display: "flex",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center", alignItems: "center",
marginBottom: "15px",
}} }}
> >
{p.title && <Typography variant="h5">{p.title}</Typography>} {p.title && (
<Typography variant="h5" style={{ marginBottom: "15px" }}>
{p.title}
</Typography>
)}
{p.actions} {p.actions}
</span> </span>
)} )}

View File

@ -25,8 +25,6 @@ export function OEMStringFormWidget(p: {
p.onChange?.(); p.onChange?.();
}; };
if (!p.editable && p.vm.oem_strings.length === 0) return <></>;
return ( return (
<EditSection <EditSection
title="SMBIOS OEM Strings" title="SMBIOS OEM Strings"

View File

@ -17,11 +17,8 @@ export function SelectInput(p: {
value?: string; value?: string;
editable: boolean; editable: boolean;
label?: string; label?: string;
size?: "medium" | "small";
options: SelectOption[]; options: SelectOption[];
onValueChange: (o?: string) => void; onValueChange: (o?: string) => void;
disableUnderline?: boolean;
disableBottomMargin?: boolean;
}): React.ReactElement { }): React.ReactElement {
if (!p.editable && !p.value) return <></>; if (!p.editable && !p.value) return <></>;
@ -31,18 +28,12 @@ export function SelectInput(p: {
} }
return ( return (
<FormControl <FormControl fullWidth variant="standard" style={{ marginBottom: "15px" }}>
fullWidth
variant="standard"
style={{ marginBottom: p.disableBottomMargin ? "0px" : "15px" }}
>
{p.label && <InputLabel>{p.label}</InputLabel>} {p.label && <InputLabel>{p.label}</InputLabel>}
<Select <Select
{...p}
value={p.value ?? ""} value={p.value ?? ""}
onChange={(e) => { label={p.label}
p.onValueChange(e.target.value); onChange={(e) => { p.onValueChange(e.target.value); }}
}}
> >
{p.options.map((e) => ( {p.options.map((e) => (
<MenuItem <MenuItem

View File

@ -18,7 +18,6 @@ export function TextInput(p: {
style?: React.CSSProperties; style?: React.CSSProperties;
helperText?: string; helperText?: string;
disabled?: boolean; disabled?: boolean;
endAdornment?: React.ReactNode;
}): React.ReactElement { }): React.ReactElement {
if (!p.editable && (p.value ?? "") === "") return <></>; if (!p.editable && (p.value ?? "") === "") return <></>;
@ -52,7 +51,6 @@ export function TextInput(p: {
input: { input: {
readOnly: !p.editable, readOnly: !p.editable,
type: p.type, type: p.type,
endAdornment: p.endAdornment,
}, },
}} }}
variant={"standard"} variant={"standard"}

View File

@ -1,22 +1,28 @@
import { mdiHarddiskPlus } from "@mdi/js"; import { mdiHarddisk, mdiHarddiskPlus } from "@mdi/js";
import Icon from "@mdi/react"; import Icon from "@mdi/react";
import CheckCircleIcon from "@mui/icons-material/CheckCircle"; import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import DeleteIcon from "@mui/icons-material/Delete"; import DeleteIcon from "@mui/icons-material/Delete";
import ExpandIcon from "@mui/icons-material/Expand"; import {
import { Button, IconButton, Paper, Tooltip, Typography } from "@mui/material"; Avatar,
Button,
IconButton,
ListItem,
ListItemAvatar,
ListItemText,
Paper,
Tooltip,
} from "@mui/material";
import { filesize } from "filesize";
import React from "react"; import React from "react";
import { DiskImage } from "../../api/DiskImageApi";
import { ServerApi } from "../../api/ServerApi"; import { ServerApi } from "../../api/ServerApi";
import { VMFileDisk, VMInfo, VMState } from "../../api/VMApi"; import { VMFileDisk, VMInfo, VMState } from "../../api/VMApi";
import { ConvertDiskImageDialog } from "../../dialogs/ConvertDiskImageDialog"; import { ConvertDiskImageDialog } from "../../dialogs/ConvertDiskImageDialog";
import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider"; import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
import { VMDiskFileWidget } from "../vms/VMDiskFileWidget";
import { CheckboxInput } from "./CheckboxInput"; import { CheckboxInput } from "./CheckboxInput";
import { DiskBusSelect } from "./DiskBusSelect";
import { DiskImageSelect } from "./DiskImageSelect";
import { DiskSizeInput } from "./DiskSizeInput";
import { SelectInput } from "./SelectInput"; import { SelectInput } from "./SelectInput";
import { TextInput } from "./TextInput"; import { TextInput } from "./TextInput";
import { DiskImageSelect } from "./DiskImageSelect";
import { DiskImage } from "../../api/DiskImageApi";
export function VMDisksList(p: { export function VMDisksList(p: {
vm: VMInfo; vm: VMInfo;
@ -33,7 +39,6 @@ export function VMDisksList(p: {
p.vm.file_disks.push({ p.vm.file_disks.push({
format: "QCow2", format: "QCow2",
size: 10000 * 1000 * 1000, size: 10000 * 1000 * 1000,
bus: "Virtio",
delete: false, delete: false,
name: `disk${p.vm.file_disks.length}`, name: `disk${p.vm.file_disks.length}`,
new: true, new: true,
@ -69,12 +74,6 @@ export function VMDisksList(p: {
/> />
))} ))}
{p.vm.file_disks.length === 0 && (
<Typography style={{ textAlign: "center", paddingTop: "25px" }}>
No disk file yet!
</Typography>
)}
{p.editable && <Button onClick={addNewDisk}>Add new disk</Button>} {p.editable && <Button onClick={addNewDisk}>Add new disk</Button>}
{/* Disk backup */} {/* Disk backup */}
@ -101,19 +100,6 @@ function DiskInfo(p: {
diskImagesList: DiskImage[]; diskImagesList: DiskImage[];
}): React.ReactElement { }): React.ReactElement {
const confirm = useConfirm(); const confirm = useConfirm();
const expandDisk = () => {
if (p.disk.resize === true) {
p.disk.resize = false;
p.disk.size = p.disk.originalSize!;
} else {
p.disk.resize = true;
p.disk.originalSize = p.disk.size!;
}
p.onChange?.();
};
const deleteDisk = async () => { const deleteDisk = async () => {
if (p.disk.deleteType) { if (p.disk.deleteType) {
p.disk.deleteType = undefined; p.disk.deleteType = undefined;
@ -136,75 +122,66 @@ function DiskInfo(p: {
if (!p.editable || !p.disk.new) if (!p.editable || !p.disk.new)
return ( return (
<> <ListItem
<VMDiskFileWidget secondaryAction={
{...p} <>
secondaryAction={ {p.editable && (
<IconButton
edge="end"
aria-label="delete disk"
onClick={deleteDisk}
>
{p.disk.deleteType ? (
<Tooltip title="Cancel disk removal">
<CheckCircleIcon />
</Tooltip>
) : (
<Tooltip title="Remove disk">
<DeleteIcon />
</Tooltip>
)}
</IconButton>
)}
{p.canBackup && (
<Tooltip title="Backup this disk">
<IconButton
onClick={() => {
p.onRequestBackup(p.disk);
}}
>
<Icon path={mdiHarddiskPlus} size={1} />
</IconButton>
</Tooltip>
)}
</>
}
>
<ListItemAvatar>
<Avatar>
<Icon path={mdiHarddisk} />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<> <>
{p.editable && !p.disk.deleteType && ( {p.disk.name}{" "}
<IconButton {p.disk.deleteType && (
edge="end" <span style={{ color: "red" }}>
aria-label="expand disk" {p.disk.deleteType === "deletefile"
onClick={expandDisk} ? "Remove, DELETING block file"
> : "Remove, keeping block file"}
{p.disk.resize === true ? ( </span>
<Tooltip title="Cancel disk expansion">
<ExpandIcon color="error" />
</Tooltip>
) : (
<Tooltip title="Increase disk size">
<ExpandIcon />
</Tooltip>
)}
</IconButton>
)}
{p.editable && (
<IconButton
edge="end"
aria-label="delete disk"
onClick={deleteDisk}
>
{p.disk.deleteType ? (
<Tooltip title="Cancel disk removal">
<CheckCircleIcon />
</Tooltip>
) : (
<Tooltip title="Remove disk">
<DeleteIcon />
</Tooltip>
)}
</IconButton>
)}
{p.canBackup && (
<Tooltip title="Backup this disk">
<IconButton
onClick={() => {
p.onRequestBackup(p.disk);
}}
>
<Icon path={mdiHarddiskPlus} size={1} />
</IconButton>
</Tooltip>
)} )}
</> </>
} }
secondary={`${filesize(p.disk.size)} - ${p.disk.format}${
p.disk.format == "Raw"
? " - " + (p.disk.is_sparse ? "Sparse" : "Fixed")
: ""
}`}
/> />
</ListItem>
{/* New disk size*/}
{p.disk.resize && !p.disk.deleteType && (
<DiskSizeInput
editable
label="New disk size (GB)"
value={p.disk.size}
onChange={(v) => {
p.disk.size = v;
p.onChange?.();
}}
/>
)}
</>
); );
return ( return (
@ -243,17 +220,6 @@ function DiskInfo(p: {
}} }}
/> />
{/* Bus selection */}
<DiskBusSelect
editable
value={p.disk.bus}
onValueChange={(v) => {
p.disk.bus = v;
p.onChange?.();
}}
/>
{/* Raw disk: choose sparse mode */}
{p.disk.format === "Raw" && ( {p.disk.format === "Raw" && (
<CheckboxInput <CheckboxInput
editable editable
@ -266,32 +232,24 @@ function DiskInfo(p: {
/> />
)} )}
{/* Resize disk image */} <TextInput
{!!p.disk.from_image && ( editable={true}
<CheckboxInput label="Disk size (GB)"
editable size={{
checked={p.disk.resize} min:
label="Resize disk file" ServerApi.Config.constraints.disk_size.min / (1000 * 1000 * 1000),
onValueChange={(v) => { max:
p.disk.resize = v; ServerApi.Config.constraints.disk_size.max / (1000 * 1000 * 1000),
p.onChange?.(); }}
}} value={(p.disk.size / (1000 * 1000 * 1000)).toString()}
/> onValueChange={(v) => {
)} p.disk.size = Number(v ?? "0") * 1000 * 1000 * 1000;
p.onChange?.();
}}
type="number"
disabled={!!p.disk.from_image}
/>
{/* Disk size */}
{(!p.disk.from_image || p.disk.resize === true) && (
<DiskSizeInput
editable
value={p.disk.size}
onChange={(v) => {
p.disk.size = v;
p.onChange?.();
}}
/>
)}
{/* Disk image selection */}
<DiskImageSelect <DiskImageSelect
label="Use disk image as template" label="Use disk image as template"
list={p.diskImagesList} list={p.diskImagesList}

View File

@ -10,7 +10,6 @@ import {
ListItemAvatar, ListItemAvatar,
ListItemText, ListItemText,
Tooltip, Tooltip,
Typography,
} from "@mui/material"; } from "@mui/material";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import { NWFilter } from "../../api/NWFilterApi"; import { NWFilter } from "../../api/NWFilterApi";
@ -50,12 +49,6 @@ export function VMNetworksList(p: {
</div> </div>
)} )}
{p.vm.networks.length === 0 && (
<Typography style={{ textAlign: "center", paddingTop: "25px" }}>
No network interface defined yet!
</Typography>
)}
<Grid container spacing={2}> <Grid container spacing={2}>
{/* networks list */} {/* networks list */}
{p.vm.networks.map((n, num) => ( {p.vm.networks.map((n, num) => (

View File

@ -60,7 +60,6 @@ export function TokenRightsEditor(p: {
<TableCell align="center">Get XML definition</TableCell> <TableCell align="center">Get XML definition</TableCell>
<TableCell align="center">Get autostart</TableCell> <TableCell align="center">Get autostart</TableCell>
<TableCell align="center">Set autostart</TableCell> <TableCell align="center">Set autostart</TableCell>
<TableCell align="center">Get CloudInit disk</TableCell>
<TableCell align="center">Backup disk</TableCell> <TableCell align="center">Backup disk</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
@ -85,13 +84,6 @@ export function TokenRightsEditor(p: {
{...p} {...p}
right={{ verb: "PUT", path: "/api/vm/*/autostart" }} right={{ verb: "PUT", path: "/api/vm/*/autostart" }}
/> />
<CellRight
{...p}
right={{
verb: "GET",
path: "/api/vm/*/cloud_init_disk",
}}
/>
<CellRight <CellRight
{...p} {...p}
right={{ verb: "POST", path: "/api/vm/*/disk/*/backup" }} right={{ verb: "POST", path: "/api/vm/*/disk/*/backup" }}
@ -131,15 +123,7 @@ export function TokenRightsEditor(p: {
{...p} {...p}
right={{ verb: "PUT", path: `/api/vm/${v.uuid}/autostart` }} right={{ verb: "PUT", path: `/api/vm/${v.uuid}/autostart` }}
parent={{ verb: "PUT", path: "/api/vm/*/autostart" }} parent={{ verb: "PUT", path: "/api/vm/*/autostart" }}
/> />{" "}
<CellRight
{...p}
right={{
verb: "GET",
path: `/api/vm/${v.uuid}/cloud_init_disk`,
}}
parent={{ verb: "GET", path: "/api/vm/*/cloud_init_disk" }}
/>
<CellRight <CellRight
{...p} {...p}
right={{ right={{
@ -723,11 +707,6 @@ export function TokenRightsEditor(p: {
right={{ verb: "POST", path: "/api/disk_images/*/convert" }} right={{ verb: "POST", path: "/api/disk_images/*/convert" }}
label="Convert disk images" label="Convert disk images"
/> />
<RouteRight
{...p}
right={{ verb: "POST", path: "/api/disk_images/*/rename" }}
label="Rename disk images"
/>
<RouteRight <RouteRight
{...p} {...p}
right={{ verb: "DELETE", path: "/api/disk_images/*" }} right={{ verb: "DELETE", path: "/api/disk_images/*" }}
@ -799,11 +778,6 @@ export function TokenRightsEditor(p: {
right={{ verb: "GET", path: "/api/server/bridges" }} right={{ verb: "GET", path: "/api/server/bridges" }}
label="Get list of network bridges" label="Get list of network bridges"
/> />
<RouteRight
{...p}
right={{ verb: "GET", path: "/api/server/export_configs" }}
label="Export all configurations"
/>
</RightsSection> </RightsSection>
</> </>
); );

View File

@ -5,7 +5,6 @@ import Grid from "@mui/material/Grid";
import React from "react"; import React from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { validate as validateUUID } from "uuid"; import { validate as validateUUID } from "uuid";
import { DiskImage, DiskImageApi } from "../../api/DiskImageApi";
import { GroupApi } from "../../api/GroupApi"; import { GroupApi } from "../../api/GroupApi";
import { IsoFile, IsoFilesApi } from "../../api/IsoFilesApi"; import { IsoFile, IsoFilesApi } from "../../api/IsoFilesApi";
import { NWFilter, NWFilterApi } from "../../api/NWFilterApi"; import { NWFilter, NWFilterApi } from "../../api/NWFilterApi";
@ -19,7 +18,6 @@ import { AsyncWidget } from "../AsyncWidget";
import { TabsWidget } from "../TabsWidget"; import { TabsWidget } from "../TabsWidget";
import { XMLAsyncWidget } from "../XMLWidget"; import { XMLAsyncWidget } from "../XMLWidget";
import { CheckboxInput } from "../forms/CheckboxInput"; import { CheckboxInput } from "../forms/CheckboxInput";
import { CloudInitEditor } from "../forms/CloudInitEditor";
import { EditSection } from "../forms/EditSection"; import { EditSection } from "../forms/EditSection";
import { OEMStringFormWidget } from "../forms/OEMStringFormWidget"; import { OEMStringFormWidget } from "../forms/OEMStringFormWidget";
import { ResAutostartInput } from "../forms/ResAutostartInput"; import { ResAutostartInput } from "../forms/ResAutostartInput";
@ -29,6 +27,7 @@ import { VMDisksList } from "../forms/VMDisksList";
import { VMNetworksList } from "../forms/VMNetworksList"; import { VMNetworksList } from "../forms/VMNetworksList";
import { VMSelectIsoInput } from "../forms/VMSelectIsoInput"; import { VMSelectIsoInput } from "../forms/VMSelectIsoInput";
import { VMScreenshot } from "./VMScreenshot"; import { VMScreenshot } from "./VMScreenshot";
import { DiskImage, DiskImageApi } from "../../api/DiskImageApi";
interface DetailsProps { interface DetailsProps {
vm: VMInfo; vm: VMInfo;
@ -90,7 +89,6 @@ enum VMTab {
General = 0, General = 0,
Storage, Storage,
Network, Network,
CloudInit,
Advanced, Advanced,
XML, XML,
Danger, Danger,
@ -118,11 +116,6 @@ function VMDetailsInner(p: DetailsInnerProps): React.ReactElement {
{ label: "General", value: VMTab.General, visible: true }, { label: "General", value: VMTab.General, visible: true },
{ label: "Storage", value: VMTab.Storage, visible: true }, { label: "Storage", value: VMTab.Storage, visible: true },
{ label: "Network", value: VMTab.Network, visible: true }, { label: "Network", value: VMTab.Network, visible: true },
{
label: "Cloud Init",
value: VMTab.CloudInit,
visible: p.editable || p.vm.cloud_init.attach_config,
},
{ label: "Avanced", value: VMTab.Advanced, visible: true }, { label: "Avanced", value: VMTab.Advanced, visible: true },
{ {
@ -142,7 +135,6 @@ function VMDetailsInner(p: DetailsInnerProps): React.ReactElement {
{currTab === VMTab.General && <VMDetailsTabGeneral {...p} />} {currTab === VMTab.General && <VMDetailsTabGeneral {...p} />}
{currTab === VMTab.Storage && <VMDetailsTabStorage {...p} />} {currTab === VMTab.Storage && <VMDetailsTabStorage {...p} />}
{currTab === VMTab.Network && <VMDetailsTabNetwork {...p} />} {currTab === VMTab.Network && <VMDetailsTabNetwork {...p} />}
{currTab === VMTab.CloudInit && <VMDetailsTabCloudInit {...p} />}
{currTab === VMTab.Advanced && <VMDetailsTabAdvanced {...p} />} {currTab === VMTab.Advanced && <VMDetailsTabAdvanced {...p} />}
{currTab === VMTab.XML && <VMDetailsTabXML {...p} />} {currTab === VMTab.XML && <VMDetailsTabXML {...p} />}
{currTab === VMTab.Danger && <VMDetailsTabDanger {...p} />} {currTab === VMTab.Danger && <VMDetailsTabDanger {...p} />}
@ -288,7 +280,6 @@ function VMDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement {
options={[ options={[
{ label: "UEFI with Secure Boot", value: "UEFISecureBoot" }, { label: "UEFI with Secure Boot", value: "UEFISecureBoot" },
{ label: "UEFI", value: "UEFI" }, { label: "UEFI", value: "UEFI" },
{ label: "Legacy", value: "Legacy" },
]} ]}
/> />
@ -389,10 +380,6 @@ function VMDetailsTabNetwork(p: DetailsInnerProps): React.ReactElement {
return <VMNetworksList {...p} />; return <VMNetworksList {...p} />;
} }
function VMDetailsTabCloudInit(p: DetailsInnerProps): React.ReactElement {
return <CloudInitEditor {...p} />;
}
function VMDetailsTabAdvanced(p: DetailsInnerProps): React.ReactElement { function VMDetailsTabAdvanced(p: DetailsInnerProps): React.ReactElement {
return ( return (
<Grid container spacing={2}> <Grid container spacing={2}>

View File

@ -3,69 +3,18 @@ import { Icon } from "@mdi/react";
import { Avatar, ListItem, ListItemAvatar, ListItemText } from "@mui/material"; import { Avatar, ListItem, ListItemAvatar, ListItemText } from "@mui/material";
import { filesize } from "filesize"; import { filesize } from "filesize";
import { VMFileDisk } from "../../api/VMApi"; import { VMFileDisk } from "../../api/VMApi";
import { DiskBusSelect } from "../forms/DiskBusSelect";
export function VMDiskFileWidget(p: {
editable?: boolean;
disk: VMFileDisk;
secondaryAction?: React.ReactElement;
onChange?: () => void;
}): React.ReactElement {
const info = [filesize(p.disk.size), p.disk.format];
if (p.disk.format === "Raw") info.push(p.disk.is_sparse ? "Sparse" : "Fixed");
if (!p.editable) info.push(p.disk.bus);
export function VMDiskFileWidget(p: { disk: VMFileDisk }): React.ReactElement {
return ( return (
<ListItem secondaryAction={p.secondaryAction}> <ListItem>
<ListItemAvatar> <ListItemAvatar>
<Avatar> <Avatar>
<Icon path={mdiHarddisk} /> <Icon path={mdiHarddisk} />
</Avatar> </Avatar>
</ListItemAvatar> </ListItemAvatar>
<ListItemText <ListItemText
primary={ primary={p.disk.name}
<> secondary={`${p.disk.format} - ${filesize(p.disk.size)}`}
{p.disk.name}{" "}
{p.disk.deleteType && (
<span style={{ color: "red" }}>
{p.disk.deleteType === "deletefile"
? "Remove, DELETING block file"
: "Remove, keeping block file"}
</span>
)}
</>
}
secondary={
<div style={{ display: "flex", alignItems: "center" }}>
{p.editable ? (
<div
style={{
maxWidth: "80px",
display: "inline-block",
marginRight: "10px",
}}
>
<DiskBusSelect
onValueChange={(v) => {
p.disk.bus = v;
p.onChange?.();
}}
label=""
editable
value={p.disk.bus}
size="small"
disableUnderline
disableBottomMargin
/>
</div>
) : (
""
)}
<div style={{ height: "100%" }}>{info.join(" - ")}</div>
</div>
}
/> />
</ListItem> </ListItem>
); );