12 Commits

Author SHA1 Message Date
3680ceed61 Update Rust crate reqwest to 0.12.19
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-06-09 00:25:15 +00:00
8a7712ec42 Add dsmode cloud-init metadata
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-07 11:50:22 +02:00
9609cfb33a Check if file can be loaded during disk image upload
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-07 11:28:39 +02:00
1fe7c60f36 Automatically generate cloud disk image when updating domains configuration
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-07 11:15:55 +02:00
f1339f0711 Generate cloud init disk image
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-07 10:32:39 +02:00
b3f56cea81 Add cloud init configuration to VM definition
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-06 17:54:56 +02:00
9bd702d60f Add cloud-localds as a required program
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-06 17:19:11 +02:00
c8b42626a9 Update dependency @types/react-dom to ^19.1.6
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-05 00:24:08 +00:00
4ef15507d9 Update dependency react-router-dom to ^7.6.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-04 00:25:09 +00:00
8a4b3a4db6 Update materialui to ^7.1.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-03 00:25:45 +00:00
8bce9ca9b7 Update dependency @types/react to ^19.1.6
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-02 00:23:25 +00:00
5574037b73 Update dependency eslint-plugin-react-hooks to ^5.2.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-01 00:23:13 +00:00
20 changed files with 365 additions and 112 deletions

View File

@ -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"
@ -2773,9 +2788,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.15" version = "0.12.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
@ -2798,23 +2813,22 @@ dependencies = [
"once_cell", "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 +2937,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"
@ -3522,6 +3527,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"

View File

@ -27,7 +27,7 @@ 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.19", 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"] }

View File

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

View File

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

View File

@ -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,22 @@ 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";
/// 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";

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ 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::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 +95,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 +142,22 @@ impl VMInfo {
return Err(StructureExtraction("Invalid number of vCPU specified!").into()); return Err(StructureExtraction("Invalid number of vCPU specified!").into());
} }
let mut disks = vec![]; let mut iso_absolute_files = vec![];
// Add ISO files // Process cloud init image
if self.cloud_init.attach_config {
let cloud_init_disk_path = AppConfig::get().cloud_init_disk_path_for_vm(&self.name);
// Apply latest cloud init configuration
std::fs::write(
&cloud_init_disk_path,
self.cloud_init.generate_nocloud_disk()?,
)?;
iso_absolute_files.push(cloud_init_disk_path);
}
// Process uploaded ISO files
for iso_file in &self.iso_files { for iso_file in &self.iso_files {
if !files_utils::check_file_name(iso_file) { if !files_utils::check_file_name(iso_file) {
return Err(StructureExtraction("ISO filename is invalid!").into()); return Err(StructureExtraction("ISO filename is invalid!").into());
@ -152,6 +169,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 +185,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 +202,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 +365,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 +516,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 +609,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(),
}) })
} }
} }

View File

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

View File

@ -0,0 +1,96 @@
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 {
/// Generate disk image for nocloud usage
pub fn generate_nocloud_disk(&self) -> anyhow::Result<Vec<u8>> {
let temp_path = tempfile::tempdir_in(&AppConfig::get().temp_dir)?;
let mut cmd = Command::new(constants::PROGRAM_CLOUD_LOCALDS);
// ISO destination path
let temp_iso = temp_path.path().join("disk.iso");
cmd.arg(&temp_iso);
// Process network configuration
if let Some(net_conf) = &self.network_configuration {
let net_conf_path = temp_path.path().join("network");
std::fs::write(&net_conf_path, net_conf)?;
cmd.arg("--network-config").arg(&net_conf_path);
}
// Process user data
let user_data_path = temp_path.path().join("user-data");
std::fs::write(&user_data_path, &self.user_data)?;
cmd.arg(user_data_path);
// Process metadata
let mut metadatas = vec![];
if let Some(inst_id) = &self.instance_id {
metadatas.push(format!("instance-id: {}", inst_id));
}
if let Some(local_hostname) = &self.local_hostname {
metadatas.push(format!("local-hostname: {}", local_hostname));
}
if let Some(dsmode) = &self.dsmode {
metadatas.push(format!(
"dsmode: {}",
match dsmode {
CloudInitDSMode::Net => "net",
CloudInitDSMode::Local => "local",
}
));
}
let meta_data_path = temp_path.path().join("meta-data");
std::fs::write(&meta_data_path, metadatas.join("\n"))?;
cmd.arg(meta_data_path);
// Execute command
let output = cmd.output()?;
if !output.status.success() {
anyhow::bail!(
"{} exited with status {}!\nStdout: {}\nStderr: {}",
constants::PROGRAM_CLOUD_LOCALDS,
output.status,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
// Read generated ISO file
Ok(std::fs::read(temp_iso)?)
}
}

View File

@ -124,7 +124,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")
@ -161,7 +161,7 @@ impl DiskFileInfo {
let mut cmd = match (self.format, dest_format) { let mut cmd = match (self.format, dest_format) {
// Decompress QCow2 // Decompress QCow2
(DiskFileFormat::CompressedQCow2, DiskFileFormat::QCow2 { .. }) => { (DiskFileFormat::CompressedQCow2, 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")
@ -172,7 +172,7 @@ impl DiskFileInfo {
// Compress QCow2 // Compress QCow2
(DiskFileFormat::QCow2 { .. }, DiskFileFormat::CompressedQCow2) => { (DiskFileFormat::QCow2 { .. }, DiskFileFormat::CompressedQCow2) => {
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)
@ -182,7 +182,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 +201,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 +214,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 +228,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,7 +237,7 @@ 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()));
@ -246,7 +246,7 @@ impl DiskFileInfo {
// Compress Raw // Compress Raw
(DiskFileFormat::Raw { .. }, DiskFileFormat::CompressedRaw) => { (DiskFileFormat::Raw { .. }, DiskFileFormat::CompressedRaw) => {
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)
@ -256,7 +256,7 @@ impl DiskFileInfo {
// Decompress Raw to not sparse file // Decompress Raw to not sparse file
(DiskFileFormat::CompressedRaw, DiskFileFormat::Raw { is_sparse: false }) => { (DiskFileFormat::CompressedRaw, DiskFileFormat::Raw { is_sparse: false }) => {
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")
@ -268,12 +268,12 @@ impl DiskFileInfo {
// Decompress Raw to sparse file // Decompress Raw to sparse file
// https://benou.fr/www/ben/decompressing-sparse-files.html // https://benou.fr/www/ben/decompressing-sparse-files.html
(DiskFileFormat::CompressedRaw, DiskFileFormat::Raw { is_sparse: true }) => { (DiskFileFormat::CompressedRaw, 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={}", "{} -d -c {} | {} 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() temp_file.display()
)); ));
cmd cmd
@ -281,7 +281,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);
@ -341,7 +341,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 +353,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)
); );

View File

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

View File

@ -145,13 +145,13 @@ struct IPBridgeInfo {
/// Get the list of bridge interfaces /// Get the list of bridge interfaces
pub fn bridges_list() -> anyhow::Result<Vec<String>> { pub fn bridges_list() -> anyhow::Result<Vec<String>> {
let mut cmd = Command::new(constants::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)
); );

View File

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

View File

@ -12,10 +12,10 @@ The release file will be available in `virtweb_backend/target/release/virtweb_ba
This is the only artifact that must be copied to the server. It is recommended to copy it to the `/usr/local/bin` directory. This is the only artifact that must be copied to the server. It is recommended to copy it to the `/usr/local/bin` directory.
## Install requirements ## Install requirements
In order to work properly, VirtWeb relies on `libvirt`, `qemu` 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

View File

@ -13,8 +13,8 @@
"@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", "@mui/icons-material": "^7.1.1",
"@mui/material": "^7.1.0", "@mui/material": "^7.1.1",
"@mui/x-charts": "^8.3.1", "@mui/x-charts": "^8.3.1",
"@mui/x-data-grid": "^8.3.1", "@mui/x-data-grid": "^8.3.1",
"date-and-time": "^3.6.0", "date-and-time": "^3.6.0",
@ -22,7 +22,7 @@
"humanize-duration": "^3.32.2", "humanize-duration": "^3.32.2",
"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 +32,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",
@ -959,9 +959,9 @@
} }
}, },
"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 +969,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 +984,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 +995,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 +1023,7 @@
"peerDependencies": { "peerDependencies": {
"@emotion/react": "^11.5.0", "@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0", "@emotion/styled": "^11.3.0",
"@mui/material-pigment-css": "^7.1.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 +1044,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 +1071,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 +1105,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 +1145,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 +1162,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 +1598,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": {
@ -3989,9 +3989,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 +4011,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"

View File

@ -15,8 +15,8 @@
"@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", "@mui/icons-material": "^7.1.1",
"@mui/material": "^7.1.0", "@mui/material": "^7.1.1",
"@mui/x-charts": "^8.3.1", "@mui/x-charts": "^8.3.1",
"@mui/x-data-grid": "^8.3.1", "@mui/x-data-grid": "^8.3.1",
"date-and-time": "^3.6.0", "date-and-time": "^3.6.0",
@ -24,7 +24,7 @@
"humanize-duration": "^3.32.2", "humanize-duration": "^3.32.2",
"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 +34,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",

View File

@ -82,6 +82,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 +110,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 +130,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 +149,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 +165,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: "" },
}); });
} }

View File

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