Compare commits
27 Commits
20250531v2
...
746ed48be5
Author | SHA1 | Date | |
---|---|---|---|
746ed48be5 | |||
78c525f47a | |||
978a881372 | |||
ee733b04f3 | |||
0d8ef226c1 | |||
feca07558e | |||
0de15af10e | |||
d4bc92f562 | |||
a1439689dd | |||
63126c75fa | |||
940302a825 | |||
9c374f849b | |||
2fa4d0e11b | |||
d7796e1459 | |||
759361d9f6 | |||
b2529c250a | |||
8a7712ec42 | |||
9609cfb33a | |||
1fe7c60f36 | |||
f1339f0711 | |||
b3f56cea81 | |||
9bd702d60f | |||
c8b42626a9 | |||
4ef15507d9 | |||
8a4b3a4db6 | |||
8bce9ca9b7 | |||
5574037b73 |
79
virtweb_backend/Cargo.lock
generated
79
virtweb_backend/Cargo.lock
generated
@ -1622,18 +1622,23 @@ 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]]
|
[[package]]
|
||||||
@ -1839,6 +1844,16 @@ 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"
|
||||||
@ -1982,6 +1997,16 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libyml"
|
||||||
|
version = "0.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3302702afa434ffa30847a83305f0a69d6abd74293b6554c18ec85c7ef30c980"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "light-openid"
|
name = "light-openid"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@ -2773,9 +2798,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.12.15"
|
version = "0.12.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
|
checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -2790,31 +2815,28 @@ 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-pemfile",
|
"rustls-pki-types",
|
||||||
"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]]
|
||||||
@ -2923,15 +2945,6 @@ 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"
|
||||||
@ -3093,6 +3106,21 @@ 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"
|
||||||
@ -3522,6 +3550,24 @@ 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"
|
||||||
@ -3754,6 +3800,7 @@ 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",
|
||||||
|
@ -19,15 +19,16 @@ 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.10.0"
|
actix-http = "3.11.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.15", features = ["stream"] }
|
reqwest = { version = "0.12.20", 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"] }
|
||||||
|
@ -182,6 +182,13 @@ 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);
|
||||||
|
@ -250,6 +250,19 @@ 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")
|
||||||
|
@ -30,8 +30,9 @@ 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; 3] = [
|
pub const ALLOWED_DISK_IMAGES_MIME_TYPES: [&str; 4] = [
|
||||||
"application/x-qemu-disk",
|
"application/x-qemu-disk",
|
||||||
|
"application/x-raw-disk-image",
|
||||||
"application/gzip",
|
"application/gzip",
|
||||||
"application/octet-stream",
|
"application/octet-stream",
|
||||||
];
|
];
|
||||||
@ -57,6 +58,9 @@ 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;
|
||||||
|
|
||||||
@ -122,19 +126,25 @@ 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 QEMU_IMAGE_PROGRAM: &str = "/usr/bin/qemu-img";
|
pub const PROGRAM_QEMU_IMAGE: &str = "/usr/bin/qemu-img";
|
||||||
|
|
||||||
/// IP program path
|
/// IP program path
|
||||||
pub const IP_PROGRAM: &str = "/usr/sbin/ip";
|
pub const PROGRAM_IP: &str = "/usr/sbin/ip";
|
||||||
|
|
||||||
/// Copy program path
|
/// Copy program path
|
||||||
pub const COPY_PROGRAM: &str = "/bin/cp";
|
pub const PROGRAM_COPY: &str = "/bin/cp";
|
||||||
|
|
||||||
/// Gzip program path
|
/// Gzip program path
|
||||||
pub const GZIP_PROGRAM: &str = "/usr/bin/gzip";
|
pub const PROGRAM_GZIP: &str = "/usr/bin/gzip";
|
||||||
|
|
||||||
|
/// XZ program path
|
||||||
|
pub const PROGRAM_XZ: &str = "/usr/bin/xz";
|
||||||
|
|
||||||
/// Bash program
|
/// Bash program
|
||||||
pub const BASH_PROGRAM: &str = "/usr/bin/bash";
|
pub const PROGRAM_BASH: &str = "/usr/bin/bash";
|
||||||
|
|
||||||
/// DD program
|
/// DD program
|
||||||
pub const DD_PROGRAM: &str = "/usr/bin/dd";
|
pub const PROGRAM_DD: &str = "/usr/bin/dd";
|
||||||
|
|
||||||
|
/// cloud-localds program
|
||||||
|
pub const PROGRAM_CLOUD_LOCALDS: &str = "/usr/bin/cloud-localds";
|
||||||
|
@ -55,7 +55,15 @@ 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!"))
|
||||||
}
|
}
|
||||||
|
@ -109,6 +109,28 @@ 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,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
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)]
|
||||||
@ -8,6 +9,8 @@ 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
|
||||||
|
@ -13,4 +13,6 @@ enum LibVirtStructError {
|
|||||||
ParseFilteringChain(String),
|
ParseFilteringChain(String),
|
||||||
#[error("NetworkFilterExtractionError: {0}")]
|
#[error("NetworkFilterExtractionError: {0}")]
|
||||||
NetworkFilterExtraction(String),
|
NetworkFilterExtraction(String),
|
||||||
|
#[error("CloudInitConfigurationError: {0}")]
|
||||||
|
CloudInitConfiguration(String),
|
||||||
}
|
}
|
||||||
|
@ -3,7 +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::StructureExtraction;
|
use crate::libvirt_rest_structures::LibVirtStructError::{
|
||||||
|
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::{VMDiskBus, VMDiskFormat, VMFileDisk};
|
||||||
@ -94,6 +97,9 @@ 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 {
|
||||||
@ -138,9 +144,26 @@ impl VMInfo {
|
|||||||
return Err(StructureExtraction("Invalid number of vCPU specified!").into());
|
return Err(StructureExtraction("Invalid number of vCPU specified!").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut disks = vec![];
|
if let Some(e) = self.cloud_init.check_error() {
|
||||||
|
return Err(CloudInitConfiguration(e).into());
|
||||||
|
}
|
||||||
|
|
||||||
// Add ISO files
|
let mut iso_absolute_files = vec![];
|
||||||
|
|
||||||
|
// 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());
|
||||||
@ -152,6 +175,13 @@ 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(),
|
||||||
@ -161,7 +191,7 @@ impl VMInfo {
|
|||||||
cache: "none".to_string(),
|
cache: "none".to_string(),
|
||||||
},
|
},
|
||||||
source: DiskSourceXML {
|
source: DiskSourceXML {
|
||||||
file: path.to_string_lossy().to_string(),
|
file: iso_path.to_string_lossy().to_string(),
|
||||||
},
|
},
|
||||||
target: DiskTargetXML {
|
target: DiskTargetXML {
|
||||||
dev: format!(
|
dev: format!(
|
||||||
@ -178,6 +208,7 @@ 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 {
|
||||||
@ -340,6 +371,7 @@ 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 {
|
||||||
@ -490,6 +522,7 @@ 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
|
||||||
@ -582,6 +615,13 @@ 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(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,16 +47,22 @@ 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::QEMU_IMAGE_PROGRAM,
|
constants::PROGRAM_QEMU_IMAGE,
|
||||||
"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::IP_PROGRAM,
|
constants::PROGRAM_IP,
|
||||||
"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();
|
||||||
@ -198,6 +204,10 @@ 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),
|
||||||
|
117
virtweb_backend/src/utils/cloud_init_utils.rs
Normal file
117
virtweb_backend/src/utils/cloud_init_utils.rs
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
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)?)
|
||||||
|
}
|
||||||
|
}
|
@ -28,8 +28,10 @@ pub enum DiskFileFormat {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
virtual_size: FileSize,
|
virtual_size: FileSize,
|
||||||
},
|
},
|
||||||
CompressedRaw,
|
GzCompressedRaw,
|
||||||
CompressedQCow2,
|
GzCompressedQCow2,
|
||||||
|
XzCompressedRaw,
|
||||||
|
XzCompressedQCow2,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DiskFileFormat {
|
impl DiskFileFormat {
|
||||||
@ -37,8 +39,10 @@ impl DiskFileFormat {
|
|||||||
match self {
|
match self {
|
||||||
DiskFileFormat::Raw { .. } => &["raw", ""],
|
DiskFileFormat::Raw { .. } => &["raw", ""],
|
||||||
DiskFileFormat::QCow2 { .. } => &["qcow2"],
|
DiskFileFormat::QCow2 { .. } => &["qcow2"],
|
||||||
DiskFileFormat::CompressedRaw => &["raw.gz"],
|
DiskFileFormat::GzCompressedRaw => &["raw.gz"],
|
||||||
DiskFileFormat::CompressedQCow2 => &["qcow2.gz"],
|
DiskFileFormat::GzCompressedQCow2 => &["qcow2.gz"],
|
||||||
|
DiskFileFormat::XzCompressedRaw => &["raw.xz"],
|
||||||
|
DiskFileFormat::XzCompressedQCow2 => &["qcow2.xz"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -81,9 +85,14 @@ 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::CompressedQCow2
|
DiskFileFormat::GzCompressedQCow2
|
||||||
}
|
}
|
||||||
"gz" => DiskFileFormat::CompressedRaw,
|
"gz" => DiskFileFormat::GzCompressedRaw,
|
||||||
|
"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}!"),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -124,7 +133,7 @@ impl DiskFileInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DiskFileFormat::QCow2 { virtual_size } => {
|
DiskFileFormat::QCow2 { virtual_size } => {
|
||||||
let mut cmd = Command::new(constants::QEMU_IMAGE_PROGRAM);
|
let mut cmd = Command::new(constants::PROGRAM_QEMU_IMAGE);
|
||||||
cmd.arg("create")
|
cmd.arg("create")
|
||||||
.arg("-f")
|
.arg("-f")
|
||||||
.arg("qcow2")
|
.arg("qcow2")
|
||||||
@ -159,9 +168,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
|
// Decompress QCow2 (GZIP)
|
||||||
(DiskFileFormat::CompressedQCow2, DiskFileFormat::QCow2 { .. }) => {
|
(DiskFileFormat::GzCompressedQCow2, DiskFileFormat::QCow2 { .. }) => {
|
||||||
let mut cmd = Command::new(constants::GZIP_PROGRAM);
|
let mut cmd = Command::new(constants::PROGRAM_GZIP);
|
||||||
cmd.arg("--keep")
|
cmd.arg("--keep")
|
||||||
.arg("--decompress")
|
.arg("--decompress")
|
||||||
.arg("--to-stdout")
|
.arg("--to-stdout")
|
||||||
@ -170,9 +179,30 @@ impl DiskFileInfo {
|
|||||||
cmd
|
cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compress QCow2
|
// Decompress QCow2 (XZ)
|
||||||
(DiskFileFormat::QCow2 { .. }, DiskFileFormat::CompressedQCow2) => {
|
(DiskFileFormat::XzCompressedQCow2, DiskFileFormat::QCow2 { .. }) => {
|
||||||
let mut cmd = Command::new(constants::GZIP_PROGRAM);
|
let mut cmd = Command::new(constants::PROGRAM_XZ);
|
||||||
|
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)
|
||||||
@ -182,7 +212,7 @@ 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::QEMU_IMAGE_PROGRAM);
|
let mut cmd = Command::new(constants::PROGRAM_QEMU_IMAGE);
|
||||||
cmd.arg("convert")
|
cmd.arg("convert")
|
||||||
.arg("-f")
|
.arg("-f")
|
||||||
.arg("qcow2")
|
.arg("qcow2")
|
||||||
@ -201,7 +231,7 @@ 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::QEMU_IMAGE_PROGRAM);
|
let mut cmd = Command::new(constants::PROGRAM_QEMU_IMAGE);
|
||||||
cmd.arg("convert")
|
cmd.arg("convert")
|
||||||
.arg("-f")
|
.arg("-f")
|
||||||
.arg("qcow2")
|
.arg("qcow2")
|
||||||
@ -214,7 +244,7 @@ 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::QEMU_IMAGE_PROGRAM);
|
let mut cmd = Command::new(constants::PROGRAM_QEMU_IMAGE);
|
||||||
cmd.arg("convert")
|
cmd.arg("convert")
|
||||||
.arg("-f")
|
.arg("-f")
|
||||||
.arg("raw")
|
.arg("raw")
|
||||||
@ -228,7 +258,7 @@ impl DiskFileInfo {
|
|||||||
|
|
||||||
// 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::COPY_PROGRAM);
|
let mut cmd = Command::new(constants::PROGRAM_COPY);
|
||||||
cmd.arg("--sparse=never")
|
cmd.arg("--sparse=never")
|
||||||
.arg(&self.file_path)
|
.arg(&self.file_path)
|
||||||
.arg(&temp_file);
|
.arg(&temp_file);
|
||||||
@ -237,16 +267,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::DD_PROGRAM);
|
let mut cmd = Command::new(constants::PROGRAM_DD);
|
||||||
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
|
// Compress Raw (Gz)
|
||||||
(DiskFileFormat::Raw { .. }, DiskFileFormat::CompressedRaw) => {
|
(DiskFileFormat::Raw { .. }, DiskFileFormat::GzCompressedRaw) => {
|
||||||
let mut cmd = Command::new(constants::GZIP_PROGRAM);
|
let mut cmd = Command::new(constants::PROGRAM_GZIP);
|
||||||
cmd.arg("--keep")
|
cmd.arg("--keep")
|
||||||
.arg("--to-stdout")
|
.arg("--to-stdout")
|
||||||
.arg(&self.file_path)
|
.arg(&self.file_path)
|
||||||
@ -254,9 +284,29 @@ impl DiskFileInfo {
|
|||||||
cmd
|
cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decompress Raw to not sparse file
|
// Compress Raw (Xz)
|
||||||
(DiskFileFormat::CompressedRaw, DiskFileFormat::Raw { is_sparse: false }) => {
|
(DiskFileFormat::Raw { .. }, DiskFileFormat::XzCompressedRaw) => {
|
||||||
let mut cmd = Command::new(constants::GZIP_PROGRAM);
|
let mut cmd = Command::new(constants::PROGRAM_XZ);
|
||||||
|
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")
|
||||||
@ -265,15 +315,29 @@ impl DiskFileInfo {
|
|||||||
cmd
|
cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decompress Raw to sparse file
|
// Decompress Raw (Gz) to sparse file
|
||||||
// https://benou.fr/www/ben/decompressing-sparse-files.html
|
// https://benou.fr/www/ben/decompressing-sparse-files.html
|
||||||
(DiskFileFormat::CompressedRaw, DiskFileFormat::Raw { is_sparse: true }) => {
|
(DiskFileFormat::GzCompressedRaw, DiskFileFormat::Raw { is_sparse: true }) => {
|
||||||
let mut cmd = Command::new(constants::BASH_PROGRAM);
|
let mut cmd = Command::new(constants::PROGRAM_BASH);
|
||||||
cmd.arg("-c").arg(format!(
|
cmd.arg("-c").arg(format!(
|
||||||
"{} -d -c {} | {} conv=sparse of={}",
|
"{} --decompress --to-stdout {} | {} conv=sparse of={}",
|
||||||
constants::GZIP_PROGRAM,
|
constants::PROGRAM_GZIP,
|
||||||
self.file_path.display(),
|
self.file_path.display(),
|
||||||
constants::DD_PROGRAM,
|
constants::PROGRAM_DD,
|
||||||
|
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
|
||||||
@ -281,7 +345,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::COPY_PROGRAM);
|
let mut cmd = Command::new(constants::PROGRAM_COPY);
|
||||||
cmd.arg("--sparse=auto")
|
cmd.arg("--sparse=auto")
|
||||||
.arg(&self.file_path)
|
.arg(&self.file_path)
|
||||||
.arg(&temp_file);
|
.arg(&temp_file);
|
||||||
@ -330,6 +394,44 @@ 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)]
|
||||||
@ -341,7 +443,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::QEMU_IMAGE_PROGRAM);
|
let mut cmd = Command::new(constants::PROGRAM_QEMU_IMAGE);
|
||||||
cmd.args([
|
cmd.args([
|
||||||
"info",
|
"info",
|
||||||
path.to_str().unwrap_or(""),
|
path.to_str().unwrap_or(""),
|
||||||
@ -353,7 +455,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::QEMU_IMAGE_PROGRAM,
|
constants::PROGRAM_QEMU_IMAGE,
|
||||||
output.status,
|
output.status,
|
||||||
String::from_utf8_lossy(&output.stderr)
|
String::from_utf8_lossy(&output.stderr)
|
||||||
);
|
);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
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;
|
||||||
|
@ -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::IP_PROGRAM);
|
let mut cmd = Command::new(constants::PROGRAM_IP);
|
||||||
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::IP_PROGRAM,
|
constants::PROGRAM_IP,
|
||||||
output.status,
|
output.status,
|
||||||
String::from_utf8_lossy(&output.stderr)
|
String::from_utf8_lossy(&output.stderr)
|
||||||
);
|
);
|
||||||
|
@ -44,6 +44,9 @@ pub struct VMFileDisk {
|
|||||||
/// 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,
|
||||||
}
|
}
|
||||||
@ -78,6 +81,7 @@ impl VMFileDisk {
|
|||||||
|
|
||||||
delete: false,
|
delete: false,
|
||||||
from_image: None,
|
from_image: None,
|
||||||
|
resize: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,28 +148,40 @@ 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
|
||||||
|
match &self.from_image {
|
||||||
|
// Create disk file
|
||||||
|
None => {
|
||||||
|
DiskFileInfo::create(&file, format, self.size)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore disk image template
|
||||||
|
Some(disk_img) => {
|
||||||
|
let src_file =
|
||||||
|
DiskFileInfo::load_file(&AppConfig::get().disk_images_file_path(disk_img))?;
|
||||||
|
src_file.convert(&file, format)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let format = match self.format {
|
// Resize disk file if requested
|
||||||
VMDiskFormat::Raw { is_sparse } => DiskFileFormat::Raw { is_sparse },
|
if self.resize == Some(true) {
|
||||||
VMDiskFormat::QCow2 => DiskFileFormat::QCow2 {
|
let disk = DiskFileInfo::load_file(&file)?;
|
||||||
virtual_size: self.size,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create / Restore disk file
|
// Can only increase disk size
|
||||||
match &self.from_image {
|
if let Err(e) = disk.resize(self.size) {
|
||||||
// Create disk file
|
log::error!("Failed to resize disk file {}: {e:?}", self.name);
|
||||||
None => {
|
|
||||||
DiskFileInfo::create(&file, format, self.size)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore disk image template
|
|
||||||
Some(disk_img) => {
|
|
||||||
let src_file =
|
|
||||||
DiskFileInfo::load_file(&AppConfig::get().disk_images_file_path(disk_img))?;
|
|
||||||
src_file.convert(&file, format)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,9 +5,9 @@
|
|||||||
sudo apt install libvirt-dev
|
sudo apt install libvirt-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Libvirt must also be installed:
|
2. Libvirt and cloud image utilities must also be installed:
|
||||||
```bash
|
```bash
|
||||||
sudo apt install qemu-kvm libvirt-daemon-system
|
sudo apt install qemu-kvm libvirt-daemon-system cloud-image-utils
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Allow the current user to manage VMs:
|
3. Allow the current user to manage VMs:
|
||||||
|
@ -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` and `kvm`:
|
In order to work properly, VirtWeb relies on `libvirt`, `qemu`, `kvm` and `cloud-localds`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt install qemu-kvm libvirt-daemon-system libvirt0 libvirt-clients libvirt-daemon bridge-utils
|
sudo apt install qemu-kvm libvirt-daemon-system libvirt0 libvirt-clients libvirt-daemon bridge-utils cloud-image-utils
|
||||||
```
|
```
|
||||||
|
|
||||||
## Dedicated user
|
## Dedicated user
|
||||||
|
287
virtweb_frontend/package-lock.json
generated
287
virtweb_frontend/package-lock.json
generated
@ -13,16 +13,19 @@
|
|||||||
"@fontsource/roboto": "^5.2.5",
|
"@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",
|
||||||
"@mui/icons-material": "^7.1.0",
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@mui/material": "^7.1.0",
|
"@mui/icons-material": "^7.1.1",
|
||||||
|
"@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.0",
|
"react-router-dom": "^7.6.2",
|
||||||
"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",
|
||||||
@ -32,14 +35,14 @@
|
|||||||
"@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.5",
|
"@types/react": "^19.1.6",
|
||||||
"@types/react-dom": "^19.1.5",
|
"@types/react-dom": "^19.1.6",
|
||||||
"@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",
|
||||||
"eslint": "^9.27.0",
|
"eslint": "^9.27.0",
|
||||||
"eslint-plugin-react-dom": "^1.49.0",
|
"eslint-plugin-react-dom": "^1.49.0",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.20",
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
"eslint-plugin-react-x": "^1.49.0",
|
"eslint-plugin-react-x": "^1.49.0",
|
||||||
"globals": "^16.1.0",
|
"globals": "^16.1.0",
|
||||||
@ -958,10 +961,33 @@
|
|||||||
"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.0",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.1.1.tgz",
|
||||||
"integrity": "sha512-E0OqhZv548Qdc0PwWhLVA2zmjJZSTvaL4ZhoswmI8NJEC1tpW2js6LLP827jrW9MEiXYdz3QS6+hask83w74yQ==",
|
"integrity": "sha512-yBckQs4aQ8mqukLnPC6ivIRv6guhaXi8snVl00VtyojBbm+l6VbVhyTSZ68Abcx7Ah8B+GZhrB7BOli+e+9LkQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@ -969,9 +995,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/icons-material": {
|
"node_modules/@mui/icons-material": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.1.1.tgz",
|
||||||
"integrity": "sha512-1mUPMAZ+Qk3jfgL5ftRR06ATH/Esi0izHl1z56H+df6cwIlCWG66RXciUqeJCttbOXOQ5y2DCjLZI/4t3Yg3LA==",
|
"integrity": "sha512-X37+Yc8QpEnl0sYmz+WcLFy2dWgNRzbswDzLPXG7QU1XDVlP5TPp1HXjdmCupOWLL/I9m1fyhcyZl8/HPpp/Cg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.27.1"
|
"@babel/runtime": "^7.27.1"
|
||||||
@ -984,7 +1010,7 @@
|
|||||||
"url": "https://opencollective.com/mui-org"
|
"url": "https://opencollective.com/mui-org"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@mui/material": "^7.1.0",
|
"@mui/material": "^7.1.1",
|
||||||
"@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"
|
||||||
},
|
},
|
||||||
@ -995,16 +1021,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/material": {
|
"node_modules/@mui/material": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.1.1.tgz",
|
||||||
"integrity": "sha512-ahUJdrhEv+mCp4XHW+tHIEYzZMSRLg8z4AjUOsj44QpD1ZaMxQoVOG2xiHvLFdcsIPbgSRx1bg1eQSheHBgvtg==",
|
"integrity": "sha512-mTpdmdZCaHCGOH3SrYM41+XKvNL0iQfM9KlYgpSjgadXx/fEKhhvOktxm8++Xw6FFeOHoOiV+lzOI8X1rsv71A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.27.1",
|
"@babel/runtime": "^7.27.1",
|
||||||
"@mui/core-downloads-tracker": "^7.1.0",
|
"@mui/core-downloads-tracker": "^7.1.1",
|
||||||
"@mui/system": "^7.1.0",
|
"@mui/system": "^7.1.1",
|
||||||
"@mui/types": "^7.4.2",
|
"@mui/types": "^7.4.3",
|
||||||
"@mui/utils": "^7.1.0",
|
"@mui/utils": "^7.1.1",
|
||||||
"@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",
|
||||||
@ -1023,7 +1049,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.0",
|
"@mui/material-pigment-css": "^7.1.1",
|
||||||
"@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"
|
||||||
@ -1044,13 +1070,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/private-theming": {
|
"node_modules/@mui/private-theming": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.1.tgz",
|
||||||
"integrity": "sha512-4Kck4jxhqF6YxNwJdSae1WgDfXVg0lIH6JVJ7gtuFfuKcQCgomJxPvUEOySTFRPz1IZzwz5OAcToskRdffElDA==",
|
"integrity": "sha512-M8NbLUx+armk2ZuaxBkkMk11ultnWmrPlN0Xe3jUEaBChg/mcxa5HWIWS1EE4DF36WRACaAHVAvyekWlDQf0PQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.27.1",
|
"@babel/runtime": "^7.27.1",
|
||||||
"@mui/utils": "^7.1.0",
|
"@mui/utils": "^7.1.1",
|
||||||
"prop-types": "^15.8.1"
|
"prop-types": "^15.8.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -1071,9 +1097,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/styled-engine": {
|
"node_modules/@mui/styled-engine": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.1.1.tgz",
|
||||||
"integrity": "sha512-m0mJ0c6iRC+f9hMeRe0W7zZX1wme3oUX0+XTVHjPG7DJz6OdQ6K/ggEOq7ZdwilcpdsDUwwMfOmvO71qDkYd2w==",
|
"integrity": "sha512-R2wpzmSN127j26HrCPYVQ53vvMcT5DaKLoWkrfwUYq3cYytL6TQrCH8JBH3z79B6g4nMZZVoaXrxO757AlShaw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.27.1",
|
"@babel/runtime": "^7.27.1",
|
||||||
@ -1105,16 +1131,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/system": {
|
"node_modules/@mui/system": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/system/-/system-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/system/-/system-7.1.1.tgz",
|
||||||
"integrity": "sha512-iedAWgRJMCxeMHvkEhsDlbvkK+qKf9me6ofsf7twk/jfT4P1ImVf7Rwb5VubEA0sikrVL+1SkoZM41M4+LNAVA==",
|
"integrity": "sha512-Kj1uhiqnj4Zo7PDjAOghtXJtNABunWvhcRU0O7RQJ7WOxeynoH6wXPcilphV8QTFtkKaip8EiNJRiCD+B3eROA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.27.1",
|
"@babel/runtime": "^7.27.1",
|
||||||
"@mui/private-theming": "^7.1.0",
|
"@mui/private-theming": "^7.1.1",
|
||||||
"@mui/styled-engine": "^7.1.0",
|
"@mui/styled-engine": "^7.1.1",
|
||||||
"@mui/types": "^7.4.2",
|
"@mui/types": "^7.4.3",
|
||||||
"@mui/utils": "^7.1.0",
|
"@mui/utils": "^7.1.1",
|
||||||
"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"
|
||||||
@ -1145,9 +1171,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/types": {
|
"node_modules/@mui/types": {
|
||||||
"version": "7.4.2",
|
"version": "7.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.3.tgz",
|
||||||
"integrity": "sha512-edRc5JcLPsrlNFYyTPxds+d5oUovuUxnnDtpJUbP6WMeV4+6eaX/mqai1ZIWT62lCOe0nlrON0s9HDiv5en5bA==",
|
"integrity": "sha512-2UCEiK29vtiZTeLdS2d4GndBKacVyxGvReznGXGr+CzW/YhjIX+OHUdCIczZjzcRAgKBGmE9zCIgoV9FleuyRQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.27.1"
|
"@babel/runtime": "^7.27.1"
|
||||||
@ -1162,13 +1188,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/utils": {
|
"node_modules/@mui/utils": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.1.tgz",
|
||||||
"integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==",
|
"integrity": "sha512-BkOt2q7MBYl7pweY2JWwfrlahhp+uGLR8S+EhiyRaofeRYUWL2YKbSGQvN4hgSN1i8poN0PaUiii1kEMrchvzg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.27.1",
|
"@babel/runtime": "^7.27.1",
|
||||||
"@mui/types": "^7.4.2",
|
"@mui/types": "^7.4.3",
|
||||||
"@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",
|
||||||
@ -1598,18 +1624,18 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.1.5",
|
"version": "19.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.6.tgz",
|
||||||
"integrity": "sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g==",
|
"integrity": "sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q==",
|
||||||
"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.5",
|
"version": "19.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz",
|
||||||
"integrity": "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==",
|
"integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@ -3481,6 +3507,12 @@
|
|||||||
"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",
|
||||||
@ -3607,6 +3639,84 @@
|
|||||||
"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",
|
||||||
@ -3753,6 +3863,12 @@
|
|||||||
"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",
|
||||||
@ -3846,6 +3962,21 @@
|
|||||||
"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",
|
||||||
@ -3989,9 +4120,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "7.6.0",
|
"version": "7.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.2.tgz",
|
||||||
"integrity": "sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==",
|
"integrity": "sha512-U7Nv3y+bMimgWjhlT5CRdzHPu2/KVmqPwKUCChW8en5P3znxUqwlYFlbmyj8Rgp1SF6zs5X4+77kBVknkg6a0w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cookie": "^1.0.1",
|
"cookie": "^1.0.1",
|
||||||
@ -4011,12 +4142,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router-dom": {
|
"node_modules/react-router-dom": {
|
||||||
"version": "7.6.0",
|
"version": "7.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.2.tgz",
|
||||||
"integrity": "sha512-DYgm6RDEuKdopSyGOWZGtDfSm7Aofb8CCzgkliTjtu/eDuB0gcsv6qdFhhi8HdtmA+KHkt5MfZ5K2PdzjugYsA==",
|
"integrity": "sha512-Q8zb6VlTbdYKK5JJBLQEN06oTUa/RAbG/oQS1auK1I0TbJOXktqm+QENEVJU6QvWynlXPRBXI3fiOQcSEA78rA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-router": "7.6.0"
|
"react-router": "7.6.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
@ -4326,6 +4457,12 @@
|
|||||||
"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",
|
||||||
@ -4714,6 +4851,43 @@
|
|||||||
"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",
|
||||||
@ -4781,10 +4955,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
|
@ -15,16 +15,19 @@
|
|||||||
"@fontsource/roboto": "^5.2.5",
|
"@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",
|
||||||
"@mui/icons-material": "^7.1.0",
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@mui/material": "^7.1.0",
|
"@mui/icons-material": "^7.1.1",
|
||||||
|
"@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.0",
|
"react-router-dom": "^7.6.2",
|
||||||
"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",
|
||||||
@ -34,14 +37,14 @@
|
|||||||
"@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.5",
|
"@types/react": "^19.1.6",
|
||||||
"@types/react-dom": "^19.1.5",
|
"@types/react-dom": "^19.1.6",
|
||||||
"@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",
|
||||||
"eslint": "^9.27.0",
|
"eslint": "^9.27.0",
|
||||||
"eslint-plugin-react-dom": "^1.49.0",
|
"eslint-plugin-react-dom": "^1.49.0",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.20",
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
"eslint-plugin-react-x": "^1.49.0",
|
"eslint-plugin-react-x": "^1.49.0",
|
||||||
"globals": "^16.1.0",
|
"globals": "^16.1.0",
|
||||||
|
@ -4,8 +4,10 @@ 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: "CompressedQCow2" }
|
| { format: "GzCompressedQCow2" }
|
||||||
| { format: "CompressedRaw" };
|
| { format: "GzCompressedRaw" }
|
||||||
|
| { format: "XzCompressedQCow2" }
|
||||||
|
| { format: "XzCompressedRaw" };
|
||||||
|
|
||||||
export type DiskImage = {
|
export type DiskImage = {
|
||||||
file_size: number;
|
file_size: number;
|
||||||
|
@ -31,8 +31,12 @@ export interface BaseFileVMDisk {
|
|||||||
// 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";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,6 +86,15 @@ 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";
|
export type VMBootType = "UEFI" | "UEFISecureBoot" | "Legacy";
|
||||||
|
|
||||||
interface VMInfoInterface {
|
interface VMInfoInterface {
|
||||||
@ -101,6 +114,7 @@ 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 {
|
||||||
@ -120,6 +134,7 @@ 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;
|
||||||
@ -138,6 +153,7 @@ 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 {
|
||||||
@ -153,6 +169,7 @@ export class VMInfo implements VMInfoInterface {
|
|||||||
networks: [],
|
networks: [],
|
||||||
tpm_module: true,
|
tpm_module: true,
|
||||||
oem_strings: [],
|
oem_strings: [],
|
||||||
|
cloud_init: { attach_config: false, user_data: "" },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,13 +42,15 @@ 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 === "CompressedQCow2") setFilename(`${origFilename}.qcow2.gz`);
|
if (value === "GzCompressedQCow2") 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 === "CompressedRaw") setFilename(`${origFilename}.raw.gz`);
|
if (value === "GzCompressedRaw") setFilename(`${origFilename}.raw.gz`);
|
||||||
|
if (value === "XzCompressedRaw") setFilename(`${origFilename}.raw.xz`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
@ -104,8 +106,10 @@ export function ConvertDiskImageDialog(
|
|||||||
options={[
|
options={[
|
||||||
{ value: "QCow2" },
|
{ value: "QCow2" },
|
||||||
{ value: "Raw" },
|
{ value: "Raw" },
|
||||||
{ value: "CompressedRaw" },
|
{ value: "GzCompressedRaw" },
|
||||||
{ value: "CompressedQCow2" },
|
{ value: "XzCompressedRaw" },
|
||||||
|
{ value: "GzCompressedQCow2" },
|
||||||
|
{ value: "XzCompressedQCow2" },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -3,16 +3,44 @@ 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 { LoadingMessageProvider } from "./hooks/providers/LoadingMessageProvider";
|
import { loader } from "@monaco-editor/react";
|
||||||
import { AlertDialogProvider } from "./hooks/providers/AlertDialogProvider";
|
import * as monaco from "monaco-editor";
|
||||||
import { SnackbarProvider } from "./hooks/providers/SnackbarProvider";
|
import EditorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
|
||||||
import { ConfirmDialogProvider } from "./hooks/providers/ConfirmDialogProvider";
|
import { configureMonacoYaml } from "monaco-yaml";
|
||||||
|
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: {
|
||||||
@ -20,9 +48,7 @@ const darkTheme = createTheme({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
const root = ReactDOM.createRoot(document.getElementById("root")!);
|
||||||
document.getElementById("root")!
|
|
||||||
);
|
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ThemeProvider theme={darkTheme}>
|
<ThemeProvider theme={darkTheme}>
|
||||||
|
@ -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,7 +36,9 @@ 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 = () => { setShowPassword((show) => !show); };
|
const handleClickShowPassword = () => {
|
||||||
|
setShowPassword((show) => !show);
|
||||||
|
};
|
||||||
|
|
||||||
const handleMouseDownPassword = (
|
const handleMouseDownPassword = (
|
||||||
event: React.MouseEvent<HTMLButtonElement>
|
event: React.MouseEvent<HTMLButtonElement>
|
||||||
@ -105,12 +107,14 @@ export function LoginRoute(): React.ReactElement {
|
|||||||
label="Username"
|
label="Username"
|
||||||
name="username"
|
name="username"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => { setUsername(e.target.value); }}
|
onChange={(e) => {
|
||||||
|
setUsername(e.target.value);
|
||||||
|
}}
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormControl fullWidth variant="outlined">
|
<FormControl required fullWidth variant="outlined">
|
||||||
<InputLabel htmlFor="password">Password</InputLabel>
|
<InputLabel htmlFor="password">Password</InputLabel>
|
||||||
<OutlinedInput
|
<OutlinedInput
|
||||||
required
|
required
|
||||||
@ -120,7 +124,9 @@ export function LoginRoute(): React.ReactElement {
|
|||||||
type={showPassword ? "text" : "password"}
|
type={showPassword ? "text" : "password"}
|
||||||
id="password"
|
id="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => { setPassword(e.target.value); }}
|
onChange={(e) => {
|
||||||
|
setPassword(e.target.value);
|
||||||
|
}}
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
endAdornment={
|
endAdornment={
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
@ -131,7 +137,11 @@ export function LoginRoute(): React.ReactElement {
|
|||||||
onMouseDown={handleMouseDownPassword}
|
onMouseDown={handleMouseDownPassword}
|
||||||
edge="end"
|
edge="end"
|
||||||
>
|
>
|
||||||
{showPassword ? <VisibilityOffIcon /> : <VisibilityIcon />}
|
{showPassword ? (
|
||||||
|
<VisibilityOffIcon />
|
||||||
|
) : (
|
||||||
|
<VisibilityIcon />
|
||||||
|
)}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
|
128
virtweb_frontend/src/widgets/forms/CloudInitEditor.tsx
Normal file
128
virtweb_frontend/src/widgets/forms/CloudInitEditor.tsx
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import Editor from "@monaco-editor/react";
|
||||||
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||||
|
import { Grid, IconButton, InputAdornment, Tooltip } from "@mui/material";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { VMInfo } from "../../api/VMApi";
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
</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">
|
||||||
|
<Editor
|
||||||
|
theme="vs-dark"
|
||||||
|
options={{
|
||||||
|
readOnly: !p.editable,
|
||||||
|
quickSuggestions: { other: true, comments: true, strings: true },
|
||||||
|
}}
|
||||||
|
language="yaml"
|
||||||
|
height={"30vh"}
|
||||||
|
value={p.vm.cloud_init.user_data}
|
||||||
|
onChange={(v) => {
|
||||||
|
p.vm.cloud_init.user_data = v ?? "";
|
||||||
|
p.onChange?.();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</EditSection>
|
||||||
|
);
|
||||||
|
}
|
25
virtweb_frontend/src/widgets/forms/DiskSizeInput.tsx
Normal file
25
virtweb_frontend/src/widgets/forms/DiskSizeInput.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -25,6 +25,8 @@ 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"
|
||||||
|
@ -18,6 +18,7 @@ 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 <></>;
|
||||||
|
|
||||||
@ -51,6 +52,7 @@ 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"}
|
||||||
|
@ -2,7 +2,8 @@ import { 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 { Button, IconButton, Paper, Tooltip } from "@mui/material";
|
import ExpandIcon from "@mui/icons-material/Expand";
|
||||||
|
import { Button, IconButton, Paper, Tooltip, Typography } from "@mui/material";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { DiskImage } from "../../api/DiskImageApi";
|
import { DiskImage } from "../../api/DiskImageApi";
|
||||||
import { ServerApi } from "../../api/ServerApi";
|
import { ServerApi } from "../../api/ServerApi";
|
||||||
@ -13,6 +14,7 @@ import { VMDiskFileWidget } from "../vms/VMDiskFileWidget";
|
|||||||
import { CheckboxInput } from "./CheckboxInput";
|
import { CheckboxInput } from "./CheckboxInput";
|
||||||
import { DiskBusSelect } from "./DiskBusSelect";
|
import { DiskBusSelect } from "./DiskBusSelect";
|
||||||
import { DiskImageSelect } from "./DiskImageSelect";
|
import { DiskImageSelect } from "./DiskImageSelect";
|
||||||
|
import { DiskSizeInput } from "./DiskSizeInput";
|
||||||
import { SelectInput } from "./SelectInput";
|
import { SelectInput } from "./SelectInput";
|
||||||
import { TextInput } from "./TextInput";
|
import { TextInput } from "./TextInput";
|
||||||
|
|
||||||
@ -67,6 +69,12 @@ 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 */}
|
||||||
@ -93,6 +101,19 @@ 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;
|
||||||
@ -115,42 +136,75 @@ function DiskInfo(p: {
|
|||||||
|
|
||||||
if (!p.editable || !p.disk.new)
|
if (!p.editable || !p.disk.new)
|
||||||
return (
|
return (
|
||||||
<VMDiskFileWidget
|
<>
|
||||||
{...p}
|
<VMDiskFileWidget
|
||||||
secondaryAction={
|
{...p}
|
||||||
<>
|
secondaryAction={
|
||||||
{p.editable && (
|
<>
|
||||||
<IconButton
|
{p.editable && !p.disk.deleteType && (
|
||||||
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
|
<IconButton
|
||||||
onClick={() => {
|
edge="end"
|
||||||
p.onRequestBackup(p.disk);
|
aria-label="expand disk"
|
||||||
}}
|
onClick={expandDisk}
|
||||||
>
|
>
|
||||||
<Icon path={mdiHarddiskPlus} size={1} />
|
{p.disk.resize === true ? (
|
||||||
|
<Tooltip title="Cancel disk expansion">
|
||||||
|
<ExpandIcon color="error" />
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Tooltip title="Increase disk size">
|
||||||
|
<ExpandIcon />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
)}
|
||||||
)}
|
|
||||||
</>
|
{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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 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 (
|
||||||
@ -212,24 +266,32 @@ function DiskInfo(p: {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<TextInput
|
{/* Resize disk image */}
|
||||||
editable={true}
|
{!!p.disk.from_image && (
|
||||||
label="Disk size (GB)"
|
<CheckboxInput
|
||||||
size={{
|
editable
|
||||||
min:
|
checked={p.disk.resize}
|
||||||
ServerApi.Config.constraints.disk_size.min / (1000 * 1000 * 1000),
|
label="Resize disk file"
|
||||||
max:
|
onValueChange={(v) => {
|
||||||
ServerApi.Config.constraints.disk_size.max / (1000 * 1000 * 1000),
|
p.disk.resize = v;
|
||||||
}}
|
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}
|
||||||
|
@ -10,6 +10,7 @@ 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";
|
||||||
@ -49,6 +50,12 @@ 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) => (
|
||||||
|
@ -60,6 +60,7 @@ 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>
|
||||||
@ -84,6 +85,13 @@ 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" }}
|
||||||
@ -123,7 +131,15 @@ 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={{
|
||||||
|
@ -5,6 +5,7 @@ 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";
|
||||||
@ -18,6 +19,7 @@ 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";
|
||||||
@ -27,7 +29,6 @@ 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;
|
||||||
@ -89,6 +90,7 @@ enum VMTab {
|
|||||||
General = 0,
|
General = 0,
|
||||||
Storage,
|
Storage,
|
||||||
Network,
|
Network,
|
||||||
|
CloudInit,
|
||||||
Advanced,
|
Advanced,
|
||||||
XML,
|
XML,
|
||||||
Danger,
|
Danger,
|
||||||
@ -116,6 +118,11 @@ 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 },
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -135,6 +142,7 @@ 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} />}
|
||||||
@ -381,6 +389,10 @@ 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}>
|
||||||
|
Reference in New Issue
Block a user