Compare commits
1 Commits
20250531
...
9334b984ae
Author | SHA1 | Date | |
---|---|---|---|
9334b984ae |
4
virtweb_backend/Cargo.lock
generated
4
virtweb_backend/Cargo.lock
generated
@ -3413,9 +3413,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.45.0"
|
version = "1.45.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165"
|
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
@ -36,7 +36,7 @@ lazy-regex = "3.4.1"
|
|||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
image = "0.25.6"
|
image = "0.25.6"
|
||||||
rand = "0.9.1"
|
rand = "0.9.1"
|
||||||
tokio = { version = "1.45.0", features = ["rt", "time", "macros"] }
|
tokio = { version = "1.45.1", features = ["rt", "time", "macros"] }
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
ipnetwork = { version = "0.21.1", features = ["serde"] }
|
ipnetwork = { version = "0.21.1", features = ["serde"] }
|
||||||
num = "0.4.3"
|
num = "0.4.3"
|
||||||
|
@ -255,11 +255,6 @@ impl AppConfig {
|
|||||||
self.storage_path().join("disk_images")
|
self.storage_path().join("disk_images")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the path of a disk image file
|
|
||||||
pub fn disk_images_file_path(&self, name: &str) -> PathBuf {
|
|
||||||
self.disk_images_storage_path().join(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get VM vnc sockets directory
|
/// Get VM vnc sockets directory
|
||||||
pub fn vnc_sockets_path(&self) -> PathBuf {
|
pub fn vnc_sockets_path(&self) -> PathBuf {
|
||||||
self.storage_path().join("vnc")
|
self.storage_path().join("vnc")
|
||||||
|
@ -27,23 +27,20 @@ pub const ALLOWED_ISO_MIME_TYPES: [&str; 4] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
/// ISO max size
|
/// ISO max size
|
||||||
pub const ISO_MAX_SIZE: FileSize = FileSize::from_gb(10);
|
pub const ISO_MAX_SIZE: usize = 10 * 1000 * 1000 * 1000;
|
||||||
|
|
||||||
/// 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; 2] =
|
||||||
"application/x-qemu-disk",
|
["application/x-qemu-disk", "application/gzip"];
|
||||||
"application/gzip",
|
|
||||||
"application/octet-stream",
|
|
||||||
];
|
|
||||||
|
|
||||||
/// Disk image max size
|
/// Disk image max size
|
||||||
pub const DISK_IMAGE_MAX_SIZE: FileSize = FileSize::from_gb(10 * 1000);
|
pub const DISK_IMAGE_MAX_SIZE: usize = 10 * 1000 * 1000 * 1000 * 1000;
|
||||||
|
|
||||||
/// Min VM memory size
|
/// Min VM memory size (MB)
|
||||||
pub const MIN_VM_MEMORY: FileSize = FileSize::from_mb(100);
|
pub const MIN_VM_MEMORY: usize = 100;
|
||||||
|
|
||||||
/// Max VM memory size
|
/// Max VM memory size (MB)
|
||||||
pub const MAX_VM_MEMORY: FileSize = FileSize::from_gb(64);
|
pub const MAX_VM_MEMORY: usize = 64000;
|
||||||
|
|
||||||
/// Disk name min length
|
/// Disk name min length
|
||||||
pub const DISK_NAME_MIN_LEN: usize = 2;
|
pub const DISK_NAME_MIN_LEN: usize = 2;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
use crate::app_config::AppConfig;
|
use crate::app_config::AppConfig;
|
||||||
use crate::constants;
|
use crate::constants;
|
||||||
use crate::controllers::{HttpResult, LibVirtReq};
|
use crate::controllers::HttpResult;
|
||||||
use crate::libvirt_lib_structures::XMLUuid;
|
|
||||||
use crate::libvirt_rest_structures::vm::VMInfo;
|
|
||||||
use crate::utils::file_disks_utils::{DiskFileFormat, DiskFileInfo};
|
use crate::utils::file_disks_utils::{DiskFileFormat, DiskFileInfo};
|
||||||
use crate::utils::files_utils;
|
use crate::utils::files_utils;
|
||||||
use actix_files::NamedFile;
|
use actix_files::NamedFile;
|
||||||
@ -26,7 +24,7 @@ pub async fn upload(MultipartForm(mut form): MultipartForm<UploadDiskImageForm>)
|
|||||||
let file = form.files.remove(0);
|
let file = form.files.remove(0);
|
||||||
|
|
||||||
// Check uploaded file size
|
// Check uploaded file size
|
||||||
if file.size > constants::DISK_IMAGE_MAX_SIZE.as_bytes() {
|
if file.size > constants::DISK_IMAGE_MAX_SIZE {
|
||||||
return Ok(HttpResponse::BadRequest().json("Disk image max size exceeded!"));
|
return Ok(HttpResponse::BadRequest().json("Disk image max size exceeded!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +47,7 @@ pub async fn upload(MultipartForm(mut form): MultipartForm<UploadDiskImageForm>)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if a file with the same name already exists
|
// Check if a file with the same name already exists
|
||||||
let dest_path = AppConfig::get().disk_images_file_path(&file_name);
|
let dest_path = AppConfig::get().disk_images_storage_path().join(file_name);
|
||||||
if dest_path.is_file() {
|
if dest_path.is_file() {
|
||||||
return Ok(HttpResponse::Conflict().json("A file with the same name already exists!"));
|
return Ok(HttpResponse::Conflict().json("A file with the same name already exists!"));
|
||||||
}
|
}
|
||||||
@ -82,7 +80,9 @@ pub async fn download(p: web::Path<DiskFilePath>, req: HttpRequest) -> HttpResul
|
|||||||
return Ok(HttpResponse::BadRequest().json("Invalid file name!"));
|
return Ok(HttpResponse::BadRequest().json("Invalid file name!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let file_path = AppConfig::get().disk_images_file_path(&p.filename);
|
let file_path = AppConfig::get()
|
||||||
|
.disk_images_storage_path()
|
||||||
|
.join(&p.filename);
|
||||||
|
|
||||||
if !file_path.exists() {
|
if !file_path.exists() {
|
||||||
return Ok(HttpResponse::NotFound().json("Disk image does not exists!"));
|
return Ok(HttpResponse::NotFound().json("Disk image does not exists!"));
|
||||||
@ -107,59 +107,6 @@ pub async fn convert(
|
|||||||
return Ok(HttpResponse::BadRequest().json("Invalid src file name!"));
|
return Ok(HttpResponse::BadRequest().json("Invalid src file name!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let src_file_path = AppConfig::get().disk_images_file_path(&p.filename);
|
|
||||||
|
|
||||||
let src = DiskFileInfo::load_file(&src_file_path)?;
|
|
||||||
|
|
||||||
handle_convert_request(src, &req).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
|
||||||
pub struct BackupVMDiskPath {
|
|
||||||
uid: XMLUuid,
|
|
||||||
diskid: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Perform disk backup
|
|
||||||
pub async fn backup_disk(
|
|
||||||
client: LibVirtReq,
|
|
||||||
path: web::Path<BackupVMDiskPath>,
|
|
||||||
req: web::Json<ConvertDiskImageRequest>,
|
|
||||||
) -> HttpResult {
|
|
||||||
// Get the VM information
|
|
||||||
let info = match client.get_single_domain(path.uid).await {
|
|
||||||
Ok(i) => i,
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to get domain info! {e}");
|
|
||||||
return Ok(HttpResponse::InternalServerError().json(e.to_string()));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let vm = VMInfo::from_domain(info)?;
|
|
||||||
|
|
||||||
// Load disk information
|
|
||||||
let Some(disk) = vm
|
|
||||||
.file_disks
|
|
||||||
.into_iter()
|
|
||||||
.find(|disk| disk.name == path.diskid)
|
|
||||||
else {
|
|
||||||
return Ok(HttpResponse::NotFound()
|
|
||||||
.json(format!("Disk {} not found for vm {}", path.diskid, vm.name)));
|
|
||||||
};
|
|
||||||
|
|
||||||
let src_path = disk.disk_path(vm.uuid.expect("Missing VM uuid!"));
|
|
||||||
let src_disk = DiskFileInfo::load_file(&src_path)?;
|
|
||||||
|
|
||||||
// Perform conversion
|
|
||||||
handle_convert_request(src_disk, &req).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generic controller code that performs image conversion to create a disk image file
|
|
||||||
pub async fn handle_convert_request(
|
|
||||||
src: DiskFileInfo,
|
|
||||||
req: &ConvertDiskImageRequest,
|
|
||||||
) -> HttpResult {
|
|
||||||
// Check destination file
|
|
||||||
if !files_utils::check_file_name(&req.dest_file_name) {
|
if !files_utils::check_file_name(&req.dest_file_name) {
|
||||||
return Ok(HttpResponse::BadRequest().json("Invalid destination file name!"));
|
return Ok(HttpResponse::BadRequest().json("Invalid destination file name!"));
|
||||||
}
|
}
|
||||||
@ -172,7 +119,15 @@ pub async fn handle_convert_request(
|
|||||||
return Ok(HttpResponse::BadRequest().json("Invalid destination file extension!"));
|
return Ok(HttpResponse::BadRequest().json("Invalid destination file extension!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let dst_file_path = AppConfig::get().disk_images_file_path(&req.dest_file_name);
|
let src_file_path = AppConfig::get()
|
||||||
|
.disk_images_storage_path()
|
||||||
|
.join(&p.filename);
|
||||||
|
|
||||||
|
let src = DiskFileInfo::load_file(&src_file_path)?;
|
||||||
|
|
||||||
|
let dst_file_path = AppConfig::get()
|
||||||
|
.disk_images_storage_path()
|
||||||
|
.join(&req.dest_file_name);
|
||||||
|
|
||||||
if dst_file_path.exists() {
|
if dst_file_path.exists() {
|
||||||
return Ok(HttpResponse::Conflict().json("Specified destination file already exists!"));
|
return Ok(HttpResponse::Conflict().json("Specified destination file already exists!"));
|
||||||
@ -186,47 +141,7 @@ pub async fn handle_convert_request(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(HttpResponse::Accepted().json("Successfully converted disk file"))
|
Ok(HttpResponse::Ok().json(src))
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
|
||||||
pub struct RenameDiskImageRequest {
|
|
||||||
name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Rename disk image
|
|
||||||
pub async fn rename(
|
|
||||||
p: web::Path<DiskFilePath>,
|
|
||||||
req: web::Json<RenameDiskImageRequest>,
|
|
||||||
) -> HttpResult {
|
|
||||||
// Check source
|
|
||||||
if !files_utils::check_file_name(&p.filename) {
|
|
||||||
return Ok(HttpResponse::BadRequest().json("Invalid src file name!"));
|
|
||||||
}
|
|
||||||
let src_path = AppConfig::get().disk_images_file_path(&p.filename);
|
|
||||||
if !src_path.exists() {
|
|
||||||
return Ok(HttpResponse::NotFound().json("Disk image does not exists!"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check destination
|
|
||||||
if !files_utils::check_file_name(&req.name) {
|
|
||||||
return Ok(HttpResponse::BadRequest().json("Invalid dst file name!"));
|
|
||||||
}
|
|
||||||
let dst_path = AppConfig::get().disk_images_file_path(&req.name);
|
|
||||||
if dst_path.exists() {
|
|
||||||
return Ok(HttpResponse::Conflict().json("Destination name already exists!"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check extension
|
|
||||||
let disk = DiskFileInfo::load_file(&src_path)?;
|
|
||||||
if !disk.format.ext().iter().any(|e| req.name.ends_with(e)) {
|
|
||||||
return Ok(HttpResponse::BadRequest().json("Invalid destination file extension!"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform rename
|
|
||||||
std::fs::rename(&src_path, &dst_path)?;
|
|
||||||
|
|
||||||
Ok(HttpResponse::Accepted().finish())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete a disk image
|
/// Delete a disk image
|
||||||
@ -235,7 +150,9 @@ pub async fn delete(p: web::Path<DiskFilePath>) -> HttpResult {
|
|||||||
return Ok(HttpResponse::BadRequest().json("Invalid file name!"));
|
return Ok(HttpResponse::BadRequest().json("Invalid file name!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let file_path = AppConfig::get().disk_images_file_path(&p.filename);
|
let file_path = AppConfig::get()
|
||||||
|
.disk_images_storage_path()
|
||||||
|
.join(&p.filename);
|
||||||
|
|
||||||
if !file_path.exists() {
|
if !file_path.exists() {
|
||||||
return Ok(HttpResponse::NotFound().json("Disk image does not exists!"));
|
return Ok(HttpResponse::NotFound().json("Disk image does not exists!"));
|
||||||
|
@ -26,7 +26,7 @@ pub async fn upload_file(MultipartForm(mut form): MultipartForm<UploadIsoForm>)
|
|||||||
|
|
||||||
let file = form.files.remove(0);
|
let file = form.files.remove(0);
|
||||||
|
|
||||||
if file.size > constants::ISO_MAX_SIZE.as_bytes() {
|
if file.size > constants::ISO_MAX_SIZE {
|
||||||
log::error!("Uploaded ISO file is too large!");
|
log::error!("Uploaded ISO file is too large!");
|
||||||
return Ok(HttpResponse::BadRequest().json("File is too large!"));
|
return Ok(HttpResponse::BadRequest().json("File is too large!"));
|
||||||
}
|
}
|
||||||
@ -88,7 +88,7 @@ pub async fn upload_from_url(req: web::Json<DownloadFromURLReq>) -> HttpResult {
|
|||||||
let response = reqwest::get(&req.url).await?;
|
let response = reqwest::get(&req.url).await?;
|
||||||
|
|
||||||
if let Some(len) = response.content_length() {
|
if let Some(len) = response.content_length() {
|
||||||
if len > constants::ISO_MAX_SIZE.as_bytes() as u64 {
|
if len > constants::ISO_MAX_SIZE as u64 {
|
||||||
return Ok(HttpResponse::BadRequest().json("File is too large!"));
|
return Ok(HttpResponse::BadRequest().json("File is too large!"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,8 +71,8 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
|
|||||||
builtin_nwfilter_rules: &constants::BUILTIN_NETWORK_FILTER_RULES,
|
builtin_nwfilter_rules: &constants::BUILTIN_NETWORK_FILTER_RULES,
|
||||||
nwfilter_chains: &constants::NETWORK_CHAINS,
|
nwfilter_chains: &constants::NETWORK_CHAINS,
|
||||||
constraints: ServerConstraints {
|
constraints: ServerConstraints {
|
||||||
iso_max_size: constants::ISO_MAX_SIZE.as_bytes(),
|
iso_max_size: constants::ISO_MAX_SIZE,
|
||||||
disk_image_max_size: constants::DISK_IMAGE_MAX_SIZE.as_bytes(),
|
disk_image_max_size: constants::DISK_IMAGE_MAX_SIZE,
|
||||||
|
|
||||||
vnc_token_duration: VNC_TOKEN_LIFETIME,
|
vnc_token_duration: VNC_TOKEN_LIFETIME,
|
||||||
|
|
||||||
@ -80,8 +80,8 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
|
|||||||
vm_title_size: LenConstraints { min: 0, max: 50 },
|
vm_title_size: LenConstraints { min: 0, max: 50 },
|
||||||
group_id_size: LenConstraints { min: 3, max: 50 },
|
group_id_size: LenConstraints { min: 3, max: 50 },
|
||||||
memory_size: LenConstraints {
|
memory_size: LenConstraints {
|
||||||
min: constants::MIN_VM_MEMORY.as_bytes(),
|
min: constants::MIN_VM_MEMORY,
|
||||||
max: constants::MAX_VM_MEMORY.as_bytes(),
|
max: constants::MAX_VM_MEMORY,
|
||||||
},
|
},
|
||||||
disk_name_size: LenConstraints {
|
disk_name_size: LenConstraints {
|
||||||
min: DISK_NAME_MIN_LEN,
|
min: DISK_NAME_MIN_LEN,
|
||||||
|
@ -22,13 +22,10 @@ pub struct DomainMetadataXML {
|
|||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "os")]
|
#[serde(rename = "os")]
|
||||||
pub struct OSXML {
|
pub struct OSXML {
|
||||||
#[serde(rename = "@firmware", default, skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "@firmware", default)]
|
||||||
pub firmware: Option<String>,
|
pub firmware: String,
|
||||||
pub r#type: OSTypeXML,
|
pub r#type: OSTypeXML,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub loader: Option<OSLoaderXML>,
|
pub loader: Option<OSLoaderXML>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub bootmenu: Option<OSBootMenuXML>,
|
|
||||||
pub smbios: Option<OSSMBiosXML>,
|
pub smbios: Option<OSSMBiosXML>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,16 +49,6 @@ pub struct OSLoaderXML {
|
|||||||
pub secure: String,
|
pub secure: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Legacy boot menu information
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
|
||||||
#[serde(rename = "bootmenu")]
|
|
||||||
pub struct OSBootMenuXML {
|
|
||||||
#[serde(rename = "@enable")]
|
|
||||||
pub enable: String,
|
|
||||||
#[serde(rename = "@timeout")]
|
|
||||||
pub timeout: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// SMBIOS System information
|
/// SMBIOS System information
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "smbios")]
|
#[serde(rename = "smbios")]
|
||||||
|
@ -4,9 +4,9 @@ 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::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::files_utils::convert_size_unit_to_mb;
|
||||||
|
use crate::utils::vm_file_disks_utils::{VMDiskFormat, VMFileDisk};
|
||||||
use lazy_regex::regex;
|
use lazy_regex::regex;
|
||||||
use num::Integer;
|
use num::Integer;
|
||||||
|
|
||||||
@ -17,7 +17,6 @@ pub struct VMGroupId(pub String);
|
|||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
pub enum BootType {
|
pub enum BootType {
|
||||||
Legacy,
|
|
||||||
UEFI,
|
UEFI,
|
||||||
UEFISecureBoot,
|
UEFISecureBoot,
|
||||||
}
|
}
|
||||||
@ -30,12 +29,6 @@ pub enum VMArchitecture {
|
|||||||
X86_64,
|
X86_64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
|
||||||
pub enum NetworkInterfaceModelType {
|
|
||||||
Virtio,
|
|
||||||
E1000,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
pub struct NWFilterParam {
|
pub struct NWFilterParam {
|
||||||
name: String,
|
name: String,
|
||||||
@ -53,7 +46,6 @@ pub struct Network {
|
|||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
r#type: NetworkType,
|
r#type: NetworkType,
|
||||||
mac: String,
|
mac: String,
|
||||||
model: NetworkInterfaceModelType,
|
|
||||||
nwfilterref: Option<NWFilterRef>,
|
nwfilterref: Option<NWFilterRef>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,8 +70,8 @@ pub struct VMInfo {
|
|||||||
pub group: Option<VMGroupId>,
|
pub group: Option<VMGroupId>,
|
||||||
pub boot_type: BootType,
|
pub boot_type: BootType,
|
||||||
pub architecture: VMArchitecture,
|
pub architecture: VMArchitecture,
|
||||||
/// VM allocated RAM memory
|
/// VM allocated memory, in megabytes
|
||||||
pub memory: FileSize,
|
pub memory: usize,
|
||||||
/// Number of vCPU for the VM
|
/// Number of vCPU for the VM
|
||||||
pub number_vcpu: usize,
|
pub number_vcpu: usize,
|
||||||
/// Enable VNC access through admin console
|
/// Enable VNC access through admin console
|
||||||
@ -204,11 +196,7 @@ impl VMInfo {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let model = Some(NetIntModelXML {
|
let model = Some(NetIntModelXML {
|
||||||
r#type: match n.model {
|
r#type: "virtio".to_string(),
|
||||||
NetworkInterfaceModelType::Virtio => "virtio",
|
|
||||||
NetworkInterfaceModelType::E1000 => "e1000",
|
|
||||||
}
|
|
||||||
.to_string(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let filterref = if let Some(n) = &n.nwfilterref {
|
let filterref = if let Some(n) = &n.nwfilterref {
|
||||||
@ -314,11 +302,7 @@ impl VMInfo {
|
|||||||
"vd{}",
|
"vd{}",
|
||||||
["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"][disks.len()]
|
["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"][disks.len()]
|
||||||
),
|
),
|
||||||
bus: match disk.bus {
|
bus: "virtio".to_string(),
|
||||||
VMDiskBus::Virtio => "virtio",
|
|
||||||
VMDiskBus::SATA => "sata",
|
|
||||||
}
|
|
||||||
.to_string(),
|
|
||||||
},
|
},
|
||||||
readonly: None,
|
readonly: None,
|
||||||
boot: DiskBootXML {
|
boot: DiskBootXML {
|
||||||
@ -352,26 +336,13 @@ impl VMInfo {
|
|||||||
machine: "q35".to_string(),
|
machine: "q35".to_string(),
|
||||||
body: "hvm".to_string(),
|
body: "hvm".to_string(),
|
||||||
},
|
},
|
||||||
firmware: match self.boot_type {
|
firmware: "efi".to_string(),
|
||||||
BootType::Legacy => None,
|
loader: Some(OSLoaderXML {
|
||||||
_ => Some("efi".to_string()),
|
secure: match self.boot_type {
|
||||||
},
|
BootType::UEFI => "no".to_string(),
|
||||||
loader: match self.boot_type {
|
BootType::UEFISecureBoot => "yes".to_string(),
|
||||||
BootType::Legacy => None,
|
},
|
||||||
_ => Some(OSLoaderXML {
|
}),
|
||||||
secure: match self.boot_type {
|
|
||||||
BootType::UEFISecureBoot => "yes".to_string(),
|
|
||||||
_ => "no".to_string(),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
bootmenu: match self.boot_type {
|
|
||||||
BootType::Legacy => Some(OSBootMenuXML {
|
|
||||||
enable: "yes".to_string(),
|
|
||||||
timeout: 3000,
|
|
||||||
}),
|
|
||||||
_ => None,
|
|
||||||
},
|
|
||||||
smbios: Some(OSSMBiosXML {
|
smbios: Some(OSSMBiosXML {
|
||||||
mode: "sysinfo".to_string(),
|
mode: "sysinfo".to_string(),
|
||||||
}),
|
}),
|
||||||
@ -409,7 +380,7 @@ impl VMInfo {
|
|||||||
|
|
||||||
memory: DomainMemoryXML {
|
memory: DomainMemoryXML {
|
||||||
unit: "MB".to_string(),
|
unit: "MB".to_string(),
|
||||||
memory: self.memory.as_mb(),
|
memory: self.memory,
|
||||||
},
|
},
|
||||||
|
|
||||||
vcpu: DomainVCPUXML {
|
vcpu: DomainVCPUXML {
|
||||||
@ -463,10 +434,9 @@ impl VMInfo {
|
|||||||
.virtweb
|
.virtweb
|
||||||
.group
|
.group
|
||||||
.map(VMGroupId),
|
.map(VMGroupId),
|
||||||
boot_type: match (domain.os.loader, domain.os.bootmenu) {
|
boot_type: match domain.os.loader {
|
||||||
(_, Some(_)) => BootType::Legacy,
|
None => BootType::UEFI,
|
||||||
(None, _) => BootType::UEFI,
|
Some(l) => match l.secure.as_str() {
|
||||||
(Some(l), _) => match l.secure.as_str() {
|
|
||||||
"yes" => BootType::UEFISecureBoot,
|
"yes" => BootType::UEFISecureBoot,
|
||||||
_ => BootType::UEFI,
|
_ => BootType::UEFI,
|
||||||
},
|
},
|
||||||
@ -482,7 +452,7 @@ impl VMInfo {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
number_vcpu: domain.vcpu.body,
|
number_vcpu: domain.vcpu.body,
|
||||||
memory: FileSize::from_size_unit(&domain.memory.unit, domain.memory.memory)?,
|
memory: convert_size_unit_to_mb(&domain.memory.unit, domain.memory.memory)?,
|
||||||
vnc_access: domain.devices.graphics.is_some(),
|
vnc_access: domain.devices.graphics.is_some(),
|
||||||
iso_files: domain
|
iso_files: domain
|
||||||
.devices
|
.devices
|
||||||
@ -497,10 +467,7 @@ impl VMInfo {
|
|||||||
.disks
|
.disks
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|d| d.device == "disk")
|
.filter(|d| d.device == "disk")
|
||||||
.map(|d| {
|
.map(|d| VMFileDisk::load_from_file(&d.source.file).unwrap())
|
||||||
VMFileDisk::load_from_file(&d.source.file, &d.target.bus)
|
|
||||||
.expect("Failed to load file disk information!")
|
|
||||||
})
|
|
||||||
.collect(),
|
.collect(),
|
||||||
|
|
||||||
networks: domain
|
networks: domain
|
||||||
@ -548,18 +515,6 @@ impl VMInfo {
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
model: match d.model.as_ref() {
|
|
||||||
None => NetworkInterfaceModelType::Virtio,
|
|
||||||
Some(model) => match model.r#type.as_str() {
|
|
||||||
"virtio" => NetworkInterfaceModelType::Virtio,
|
|
||||||
"e1000" => NetworkInterfaceModelType::E1000,
|
|
||||||
model => {
|
|
||||||
return Err(LibVirtStructError::DomainExtraction(format!(
|
|
||||||
"Unknown network interface model type: {model}! "
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
nwfilterref: d.filterref.as_ref().map(|f| NWFilterRef {
|
nwfilterref: d.filterref.as_ref().map(|f| NWFilterRef {
|
||||||
name: f.filter.to_string(),
|
name: f.filter.to_string(),
|
||||||
parameters: f
|
parameters: f
|
||||||
|
@ -122,9 +122,10 @@ async fn main() -> std::io::Result<()> {
|
|||||||
}))
|
}))
|
||||||
.app_data(conn.clone())
|
.app_data(conn.clone())
|
||||||
// Uploaded files
|
// Uploaded files
|
||||||
.app_data(MultipartFormConfig::default().total_limit(
|
.app_data(
|
||||||
max(constants::DISK_IMAGE_MAX_SIZE, constants::ISO_MAX_SIZE).as_bytes(),
|
MultipartFormConfig::default()
|
||||||
))
|
.total_limit(max(constants::DISK_IMAGE_MAX_SIZE, constants::ISO_MAX_SIZE)),
|
||||||
|
)
|
||||||
.app_data(TempFileConfig::default().directory(&AppConfig::get().temp_dir))
|
.app_data(TempFileConfig::default().directory(&AppConfig::get().temp_dir))
|
||||||
// Server controller
|
// Server controller
|
||||||
.route(
|
.route(
|
||||||
@ -352,18 +353,10 @@ async fn main() -> std::io::Result<()> {
|
|||||||
"/api/disk_images/{filename}/convert",
|
"/api/disk_images/{filename}/convert",
|
||||||
web::post().to(disk_images_controller::convert),
|
web::post().to(disk_images_controller::convert),
|
||||||
)
|
)
|
||||||
.route(
|
|
||||||
"/api/disk_images/{filename}/rename",
|
|
||||||
web::post().to(disk_images_controller::rename),
|
|
||||||
)
|
|
||||||
.route(
|
.route(
|
||||||
"/api/disk_images/{filename}",
|
"/api/disk_images/{filename}",
|
||||||
web::delete().to(disk_images_controller::delete),
|
web::delete().to(disk_images_controller::delete),
|
||||||
)
|
)
|
||||||
.route(
|
|
||||||
"/api/vm/{uid}/disk/{diskid}/backup",
|
|
||||||
web::post().to(disk_images_controller::backup_disk),
|
|
||||||
)
|
|
||||||
// API tokens controller
|
// API tokens controller
|
||||||
.route(
|
.route(
|
||||||
"/api/token/create",
|
"/api/token/create",
|
||||||
|
@ -1,12 +1,3 @@
|
|||||||
use std::ops::Mul;
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
|
||||||
enum FilesSizeUtilsError {
|
|
||||||
#[error("UnitConvertError: {0}")]
|
|
||||||
UnitConvert(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Holds a data size, convertible in any form
|
|
||||||
#[derive(
|
#[derive(
|
||||||
serde::Serialize,
|
serde::Serialize,
|
||||||
serde::Deserialize,
|
serde::Deserialize,
|
||||||
@ -34,30 +25,6 @@ impl FileSize {
|
|||||||
Self(gb * 1000 * 1000 * 1000)
|
Self(gb * 1000 * 1000 * 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert size unit to MB
|
|
||||||
pub fn from_size_unit(unit: &str, value: usize) -> anyhow::Result<Self> {
|
|
||||||
let fact = match unit {
|
|
||||||
"bytes" | "b" => 1f64,
|
|
||||||
"KB" => 1000f64,
|
|
||||||
"MB" => 1000f64 * 1000f64,
|
|
||||||
"GB" => 1000f64 * 1000f64 * 1000f64,
|
|
||||||
"TB" => 1000f64 * 1000f64 * 1000f64 * 1000f64,
|
|
||||||
|
|
||||||
"k" | "KiB" => 1024f64,
|
|
||||||
"M" | "MiB" => 1024f64 * 1024f64,
|
|
||||||
"G" | "GiB" => 1024f64 * 1024f64 * 1024f64,
|
|
||||||
"T" | "TiB" => 1024f64 * 1024f64 * 1024f64 * 1024f64,
|
|
||||||
|
|
||||||
_ => {
|
|
||||||
return Err(
|
|
||||||
FilesSizeUtilsError::UnitConvert(format!("Unknown size unit: {unit}")).into(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self((value as f64).mul(fact).ceil() as usize))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get file size as bytes
|
/// Get file size as bytes
|
||||||
pub fn as_bytes(&self) -> usize {
|
pub fn as_bytes(&self) -> usize {
|
||||||
self.0
|
self.0
|
||||||
@ -68,24 +35,3 @@ impl FileSize {
|
|||||||
self.0 / (1000 * 1000)
|
self.0 / (1000 * 1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::utils::file_size_utils::FileSize;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn convert_units_mb() {
|
|
||||||
assert_eq!(FileSize::from_size_unit("MB", 1).unwrap().as_mb(), 1);
|
|
||||||
assert_eq!(FileSize::from_size_unit("MB", 1000).unwrap().as_mb(), 1000);
|
|
||||||
assert_eq!(
|
|
||||||
FileSize::from_size_unit("GB", 1000).unwrap().as_mb(),
|
|
||||||
1000 * 1000
|
|
||||||
);
|
|
||||||
assert_eq!(FileSize::from_size_unit("GB", 1).unwrap().as_mb(), 1000);
|
|
||||||
assert_eq!(FileSize::from_size_unit("GiB", 3).unwrap().as_mb(), 3221);
|
|
||||||
assert_eq!(
|
|
||||||
FileSize::from_size_unit("KiB", 488281).unwrap().as_mb(),
|
|
||||||
499
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
|
use std::ops::{Div, Mul};
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
enum FilesUtilsError {
|
||||||
|
#[error("UnitConvertError: {0}")]
|
||||||
|
UnitConvert(String),
|
||||||
|
}
|
||||||
|
|
||||||
const INVALID_CHARS: [&str; 19] = [
|
const INVALID_CHARS: [&str; 19] = [
|
||||||
"@", "\\", "/", ":", ",", "<", ">", "%", "'", "\"", "?", "{", "}", "$", "*", "|", ";", "=",
|
"@", "\\", "/", ":", ",", "<", ">", "%", "'", "\"", "?", "{", "}", "$", "*", "|", ";", "=",
|
||||||
"\t",
|
"\t",
|
||||||
@ -28,9 +35,31 @@ pub fn set_file_permission<P: AsRef<Path>>(path: P, mode: u32) -> anyhow::Result
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert size unit to MB
|
||||||
|
pub fn convert_size_unit_to_mb(unit: &str, value: usize) -> anyhow::Result<usize> {
|
||||||
|
let fact = match unit {
|
||||||
|
"bytes" | "b" => 1f64,
|
||||||
|
"KB" => 1000f64,
|
||||||
|
"MB" => 1000f64 * 1000f64,
|
||||||
|
"GB" => 1000f64 * 1000f64 * 1000f64,
|
||||||
|
"TB" => 1000f64 * 1000f64 * 1000f64 * 1000f64,
|
||||||
|
|
||||||
|
"k" | "KiB" => 1024f64,
|
||||||
|
"M" | "MiB" => 1024f64 * 1024f64,
|
||||||
|
"G" | "GiB" => 1024f64 * 1024f64 * 1024f64,
|
||||||
|
"T" | "TiB" => 1024f64 * 1024f64 * 1024f64 * 1024f64,
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
return Err(FilesUtilsError::UnitConvert(format!("Unknown size unit: {unit}")).into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((value as f64).mul(fact.div((1000 * 1000) as f64)).ceil() as usize)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::utils::files_utils::check_file_name;
|
use crate::utils::files_utils::{check_file_name, convert_size_unit_to_mb};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_file_name() {
|
fn empty_file_name() {
|
||||||
@ -56,4 +85,14 @@ mod test {
|
|||||||
fn valid_file_name() {
|
fn valid_file_name() {
|
||||||
assert!(check_file_name("test.iso"));
|
assert!(check_file_name("test.iso"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn convert_units_mb() {
|
||||||
|
assert_eq!(convert_size_unit_to_mb("MB", 1).unwrap(), 1);
|
||||||
|
assert_eq!(convert_size_unit_to_mb("MB", 1000).unwrap(), 1000);
|
||||||
|
assert_eq!(convert_size_unit_to_mb("GB", 1000).unwrap(), 1000 * 1000);
|
||||||
|
assert_eq!(convert_size_unit_to_mb("GB", 1).unwrap(), 1000);
|
||||||
|
assert_eq!(convert_size_unit_to_mb("GiB", 3).unwrap(), 3222);
|
||||||
|
assert_eq!(convert_size_unit_to_mb("KiB", 488281).unwrap(), 500);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,10 +13,11 @@ enum VMDisksError {
|
|||||||
Config(&'static str),
|
Config(&'static str),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
/// Type of disk allocation
|
||||||
pub enum VMDiskBus {
|
#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)]
|
||||||
Virtio,
|
pub enum VMDiskAllocType {
|
||||||
SATA,
|
Fixed,
|
||||||
|
Sparse,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Disk allocation type
|
/// Disk allocation type
|
||||||
@ -24,8 +25,8 @@ pub enum VMDiskBus {
|
|||||||
#[serde(tag = "format")]
|
#[serde(tag = "format")]
|
||||||
pub enum VMDiskFormat {
|
pub enum VMDiskFormat {
|
||||||
Raw {
|
Raw {
|
||||||
/// Is raw file a sparse file?
|
/// Type of disk allocation
|
||||||
is_sparse: bool,
|
alloc_type: VMDiskAllocType,
|
||||||
},
|
},
|
||||||
QCow2,
|
QCow2,
|
||||||
}
|
}
|
||||||
@ -36,20 +37,15 @@ pub struct VMFileDisk {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
/// Disk size, in bytes
|
/// Disk size, in bytes
|
||||||
pub size: FileSize,
|
pub size: FileSize,
|
||||||
/// Disk bus
|
|
||||||
pub bus: VMDiskBus,
|
|
||||||
/// Disk format
|
/// Disk format
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub format: VMDiskFormat,
|
pub format: VMDiskFormat,
|
||||||
/// When creating a new disk, specify the disk image template to use
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub from_image: Option<String>,
|
|
||||||
/// Set this variable to true to delete the disk
|
/// Set this variable to true to delete the disk
|
||||||
pub delete: bool,
|
pub delete: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VMFileDisk {
|
impl VMFileDisk {
|
||||||
pub fn load_from_file(path: &str, bus: &str) -> anyhow::Result<Self> {
|
pub fn load_from_file(path: &str) -> anyhow::Result<Self> {
|
||||||
let file = Path::new(path);
|
let file = Path::new(path);
|
||||||
|
|
||||||
let info = DiskFileInfo::load_file(file)?;
|
let info = DiskFileInfo::load_file(file)?;
|
||||||
@ -65,19 +61,16 @@ impl VMFileDisk {
|
|||||||
},
|
},
|
||||||
|
|
||||||
format: match info.format {
|
format: match info.format {
|
||||||
DiskFileFormat::Raw { is_sparse } => VMDiskFormat::Raw { is_sparse },
|
DiskFileFormat::Raw { is_sparse } => VMDiskFormat::Raw {
|
||||||
|
alloc_type: match is_sparse {
|
||||||
|
true => VMDiskAllocType::Sparse,
|
||||||
|
false => VMDiskAllocType::Fixed,
|
||||||
|
},
|
||||||
|
},
|
||||||
DiskFileFormat::QCow2 { .. } => VMDiskFormat::QCow2,
|
DiskFileFormat::QCow2 { .. } => VMDiskFormat::QCow2,
|
||||||
_ => anyhow::bail!("Unsupported image format: {:?}", info.format),
|
_ => anyhow::bail!("Unsupported image format: {:?}", info.format),
|
||||||
},
|
},
|
||||||
|
|
||||||
bus: match bus {
|
|
||||||
"virtio" => VMDiskBus::Virtio,
|
|
||||||
"sata" => VMDiskBus::SATA,
|
|
||||||
_ => anyhow::bail!("Unsupported disk bus type: {bus}"),
|
|
||||||
},
|
|
||||||
|
|
||||||
delete: false,
|
delete: false,
|
||||||
from_image: None,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,23 +90,10 @@ impl VMFileDisk {
|
|||||||
return Err(VMDisksError::Config("Disk size is invalid!").into());
|
return Err(VMDisksError::Config("Disk size is invalid!").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check specified disk image template
|
|
||||||
if let Some(disk_image) = &self.from_image {
|
|
||||||
if !files_utils::check_file_name(disk_image) {
|
|
||||||
return Err(VMDisksError::Config("Disk image template name is not valid!").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
if !AppConfig::get().disk_images_file_path(disk_image).is_file() {
|
|
||||||
return Err(
|
|
||||||
VMDisksError::Config("Specified disk image file does not exist!").into(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get disk path on file system
|
/// Get disk path
|
||||||
pub fn disk_path(&self, id: XMLUuid) -> PathBuf {
|
pub fn disk_path(&self, id: XMLUuid) -> PathBuf {
|
||||||
let domain_dir = AppConfig::get().vm_storage_path(id);
|
let domain_dir = AppConfig::get().vm_storage_path(id);
|
||||||
let file_name = match self.format {
|
let file_name = match self.format {
|
||||||
@ -147,27 +127,19 @@ impl VMFileDisk {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let format = match self.format {
|
// Create disk file
|
||||||
VMDiskFormat::Raw { is_sparse } => DiskFileFormat::Raw { is_sparse },
|
DiskFileInfo::create(
|
||||||
VMDiskFormat::QCow2 => DiskFileFormat::QCow2 {
|
&file,
|
||||||
virtual_size: self.size,
|
match self.format {
|
||||||
|
VMDiskFormat::Raw { alloc_type } => DiskFileFormat::Raw {
|
||||||
|
is_sparse: alloc_type == VMDiskAllocType::Sparse,
|
||||||
|
},
|
||||||
|
VMDiskFormat::QCow2 => DiskFileFormat::QCow2 {
|
||||||
|
virtual_size: self.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)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -103,7 +103,6 @@ export class APIClient {
|
|||||||
body: body,
|
body: body,
|
||||||
headers: headers,
|
headers: headers,
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
signal: AbortSignal.timeout(50 * 1000 * 1000),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Process response
|
// Process response
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { APIClient } from "./ApiClient";
|
import { APIClient } from "./ApiClient";
|
||||||
import { VMFileDisk, VMInfo } from "./VMApi";
|
|
||||||
|
|
||||||
export type DiskImageFormat =
|
export type DiskImageFormat =
|
||||||
| { format: "Raw"; is_sparse: boolean }
|
| { format: "Raw"; is_sparse: boolean }
|
||||||
@ -78,33 +77,6 @@ export class DiskImageApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Backup VM disk into image disks library
|
|
||||||
*/
|
|
||||||
static async BackupVMDisk(
|
|
||||||
vm: VMInfo,
|
|
||||||
disk: VMFileDisk,
|
|
||||||
dest_file_name: string,
|
|
||||||
format: DiskImageFormat
|
|
||||||
): Promise<void> {
|
|
||||||
await APIClient.exec({
|
|
||||||
uri: `/vm/${vm.uuid}/disk/${disk.name}/backup`,
|
|
||||||
method: "POST",
|
|
||||||
jsonData: { ...format, dest_file_name },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rename disk image file
|
|
||||||
*/
|
|
||||||
static async Rename(file: DiskImage, name: string): Promise<void> {
|
|
||||||
await APIClient.exec({
|
|
||||||
method: "POST",
|
|
||||||
uri: `/disk_images/${file.file_name}/rename`,
|
|
||||||
jsonData: { name },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete disk image file
|
* Delete disk image file
|
||||||
*/
|
*/
|
||||||
|
@ -19,26 +19,21 @@ export type VMState =
|
|||||||
|
|
||||||
export type VMFileDisk = BaseFileVMDisk & (RawVMDisk | QCow2Disk);
|
export type VMFileDisk = BaseFileVMDisk & (RawVMDisk | QCow2Disk);
|
||||||
|
|
||||||
export type DiskBusType = "Virtio" | "SATA";
|
|
||||||
|
|
||||||
export interface BaseFileVMDisk {
|
export interface BaseFileVMDisk {
|
||||||
size: number;
|
size: number;
|
||||||
name: string;
|
name: string;
|
||||||
bus: DiskBusType;
|
|
||||||
|
|
||||||
delete: boolean;
|
delete: boolean;
|
||||||
|
|
||||||
// For new disk only
|
// application attribute
|
||||||
from_image?: string;
|
|
||||||
|
|
||||||
// application attributes
|
|
||||||
new?: boolean;
|
new?: boolean;
|
||||||
deleteType?: "keepfile" | "deletefile";
|
deleteType?: "keepfile" | "deletefile";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DiskAllocType = "Sparse" | "Fixed";
|
||||||
|
|
||||||
interface RawVMDisk {
|
interface RawVMDisk {
|
||||||
format: "Raw";
|
format: "Raw";
|
||||||
is_sparse: boolean;
|
alloc_type: DiskAllocType;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QCow2Disk {
|
interface QCow2Disk {
|
||||||
@ -64,7 +59,6 @@ export type VMNetInterface = (
|
|||||||
|
|
||||||
export interface VMNetInterfaceBase {
|
export interface VMNetInterfaceBase {
|
||||||
mac: string;
|
mac: string;
|
||||||
model: "Virtio" | "E1000";
|
|
||||||
nwfilterref?: VMNetInterfaceFilter;
|
nwfilterref?: VMNetInterfaceFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,8 +76,6 @@ export interface VMNetBridge {
|
|||||||
bridge: string;
|
bridge: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type VMBootType = "UEFI" | "UEFISecureBoot" | "Legacy";
|
|
||||||
|
|
||||||
interface VMInfoInterface {
|
interface VMInfoInterface {
|
||||||
name: string;
|
name: string;
|
||||||
uuid?: string;
|
uuid?: string;
|
||||||
@ -91,7 +83,7 @@ interface VMInfoInterface {
|
|||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
group?: string;
|
group?: string;
|
||||||
boot_type: VMBootType;
|
boot_type: "UEFI" | "UEFISecureBoot";
|
||||||
architecture: "i686" | "x86_64";
|
architecture: "i686" | "x86_64";
|
||||||
memory: number;
|
memory: number;
|
||||||
number_vcpu: number;
|
number_vcpu: number;
|
||||||
@ -110,7 +102,7 @@ export class VMInfo implements VMInfoInterface {
|
|||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
group?: string;
|
group?: string;
|
||||||
boot_type: VMBootType;
|
boot_type: "UEFI" | "UEFISecureBoot";
|
||||||
architecture: "i686" | "x86_64";
|
architecture: "i686" | "x86_64";
|
||||||
number_vcpu: number;
|
number_vcpu: number;
|
||||||
memory: number;
|
memory: number;
|
||||||
@ -145,7 +137,7 @@ export class VMInfo implements VMInfoInterface {
|
|||||||
name: "",
|
name: "",
|
||||||
boot_type: "UEFI",
|
boot_type: "UEFI",
|
||||||
architecture: "x86_64",
|
architecture: "x86_64",
|
||||||
memory: 1000 * 1000 * 1000,
|
memory: 1024,
|
||||||
number_vcpu: 1,
|
number_vcpu: 1,
|
||||||
vnc_access: true,
|
vnc_access: true,
|
||||||
iso_files: [],
|
iso_files: [],
|
||||||
|
@ -9,69 +9,56 @@ import {
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { DiskImage, DiskImageApi, DiskImageFormat } from "../api/DiskImageApi";
|
import { DiskImage, DiskImageApi, DiskImageFormat } from "../api/DiskImageApi";
|
||||||
import { ServerApi } from "../api/ServerApi";
|
import { ServerApi } from "../api/ServerApi";
|
||||||
import { VMFileDisk, VMInfo } from "../api/VMApi";
|
|
||||||
import { useAlert } from "../hooks/providers/AlertDialogProvider";
|
import { useAlert } from "../hooks/providers/AlertDialogProvider";
|
||||||
import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider";
|
import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider";
|
||||||
|
import { useSnackbar } from "../hooks/providers/SnackbarProvider";
|
||||||
import { FileDiskImageWidget } from "../widgets/FileDiskImageWidget";
|
import { FileDiskImageWidget } from "../widgets/FileDiskImageWidget";
|
||||||
import { CheckboxInput } from "../widgets/forms/CheckboxInput";
|
import { CheckboxInput } from "../widgets/forms/CheckboxInput";
|
||||||
import { SelectInput } from "../widgets/forms/SelectInput";
|
import { SelectInput } from "../widgets/forms/SelectInput";
|
||||||
import { TextInput } from "../widgets/forms/TextInput";
|
import { TextInput } from "../widgets/forms/TextInput";
|
||||||
import { VMDiskFileWidget } from "../widgets/vms/VMDiskFileWidget";
|
|
||||||
|
|
||||||
export function ConvertDiskImageDialog(
|
export function ConvertDiskImageDialog(p: {
|
||||||
p: {
|
image: DiskImage;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onFinished: () => void;
|
onFinished: () => void;
|
||||||
} & (
|
}): React.ReactElement {
|
||||||
| { backup?: false; image: DiskImage }
|
|
||||||
| { backup: true; disk: VMFileDisk; vm: VMInfo }
|
|
||||||
)
|
|
||||||
): React.ReactElement {
|
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
|
const snackbar = useSnackbar();
|
||||||
const loadingMessage = useLoadingMessage();
|
const loadingMessage = useLoadingMessage();
|
||||||
|
|
||||||
const [format, setFormat] = React.useState<DiskImageFormat>({
|
const [format, setFormat] = React.useState<DiskImageFormat>({
|
||||||
format: "QCow2",
|
format: "QCow2",
|
||||||
});
|
});
|
||||||
|
|
||||||
const origFilename = p.backup ? p.disk.name : p.image.file_name;
|
const [filename, setFilename] = React.useState(p.image.file_name + ".qcow2");
|
||||||
|
|
||||||
const [filename, setFilename] = React.useState(origFilename + ".qcow2");
|
|
||||||
|
|
||||||
const handleFormatChange = (value?: string) => {
|
const handleFormatChange = (value?: string) => {
|
||||||
setFormat({ format: value ?? ("QCow2" as any) });
|
setFormat({ format: value ?? ("QCow2" as any) });
|
||||||
|
|
||||||
if (value === "QCow2") setFilename(`${origFilename}.qcow2`);
|
if (value === "QCow2") setFilename(`${p.image.file_name}.qcow2`);
|
||||||
if (value === "CompressedQCow2") setFilename(`${origFilename}.qcow2.gz`);
|
if (value === "CompressedQCow2")
|
||||||
|
setFilename(`${p.image.file_name}.qcow2.gz`);
|
||||||
if (value === "Raw") {
|
if (value === "Raw") {
|
||||||
setFilename(`${origFilename}.raw`);
|
setFilename(`${p.image.file_name}.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 === "CompressedRaw") setFilename(`${p.image.file_name}.raw.gz`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
loadingMessage.show(
|
loadingMessage.show("Converting image...");
|
||||||
p.backup ? "Performing backup..." : "Converting image..."
|
|
||||||
);
|
|
||||||
|
|
||||||
// Perform the conversion / backup operation
|
// Perform the conversion
|
||||||
if (p.backup)
|
await DiskImageApi.Convert(p.image, filename, format);
|
||||||
await DiskImageApi.BackupVMDisk(p.vm, p.disk, filename, format);
|
|
||||||
else await DiskImageApi.Convert(p.image, filename, format);
|
|
||||||
|
|
||||||
p.onFinished();
|
p.onFinished();
|
||||||
|
|
||||||
alert(p.backup ? "Backup successful!" : "Conversion successful!");
|
snackbar("Conversion successful!");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to perform backup/conversion!", e);
|
console.error("Failed to convert image!", e);
|
||||||
alert(
|
alert(`Failed to convert image! ${e}`);
|
||||||
p.backup
|
|
||||||
? `Failed to perform backup! ${e}`
|
|
||||||
: `Failed to convert image! ${e}`
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
loadingMessage.hide();
|
loadingMessage.hide();
|
||||||
}
|
}
|
||||||
@ -79,21 +66,13 @@ export function ConvertDiskImageDialog(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open onClose={p.onCancel}>
|
<Dialog open onClose={p.onCancel}>
|
||||||
<DialogTitle>
|
<DialogTitle>Convert disk image</DialogTitle>
|
||||||
{p.backup ? `Backup disk ${p.disk.name}` : "Convert disk image"}
|
|
||||||
</DialogTitle>
|
|
||||||
|
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
Select the destination format for this image:
|
Select the destination format for this image:
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
|
<FileDiskImageWidget image={p.image} />
|
||||||
{/* Show details of of the image */}
|
|
||||||
{p.backup ? (
|
|
||||||
<VMDiskFileWidget {...p} />
|
|
||||||
) : (
|
|
||||||
<FileDiskImageWidget {...p} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* New image format */}
|
{/* New image format */}
|
||||||
<SelectInput
|
<SelectInput
|
||||||
@ -130,13 +109,13 @@ export function ConvertDiskImageDialog(
|
|||||||
setFilename(s ?? "");
|
setFilename(s ?? "");
|
||||||
}}
|
}}
|
||||||
size={ServerApi.Config.constraints.disk_image_name_size}
|
size={ServerApi.Config.constraints.disk_image_name_size}
|
||||||
helperText="The image name shall contain the proper file extension for the selected target format"
|
helperText="The image name shall contain the proper file extension"
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={p.onCancel}>Cancel</Button>
|
<Button onClick={p.onCancel}>Cancel</Button>
|
||||||
<Button onClick={handleSubmit} autoFocus>
|
<Button onClick={handleSubmit} autoFocus>
|
||||||
{p.backup ? "Perform backup" : "Convert image"}
|
Convert image
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
import DownloadIcon from "@mui/icons-material/Download";
|
import DownloadIcon from "@mui/icons-material/Download";
|
||||||
import LoopIcon from "@mui/icons-material/Loop";
|
import LoopIcon from "@mui/icons-material/Loop";
|
||||||
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
|
||||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
@ -9,10 +8,6 @@ import {
|
|||||||
CircularProgress,
|
CircularProgress,
|
||||||
IconButton,
|
IconButton,
|
||||||
LinearProgress,
|
LinearProgress,
|
||||||
ListItemIcon,
|
|
||||||
ListItemText,
|
|
||||||
Menu,
|
|
||||||
MenuItem,
|
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
@ -169,11 +164,15 @@ function DiskImageList(p: {
|
|||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const loadingMessage = useLoadingMessage();
|
const loadingMessage = useLoadingMessage();
|
||||||
|
|
||||||
const [dlProgress, setDlProgress] = React.useState<undefined | number>();
|
|
||||||
|
|
||||||
const [currConversion, setCurrConversion] = React.useState<
|
const [currConversion, setCurrConversion] = React.useState<
|
||||||
DiskImage | undefined
|
DiskImage | undefined
|
||||||
>();
|
>();
|
||||||
|
const [dlProgress, setDlProgress] = React.useState<undefined | number>();
|
||||||
|
|
||||||
|
// Convert disk image file
|
||||||
|
const convertDiskImage = (entry: DiskImage) => {
|
||||||
|
setCurrConversion(entry);
|
||||||
|
};
|
||||||
|
|
||||||
// Download disk image file
|
// Download disk image file
|
||||||
const downloadDiskImage = async (entry: DiskImage) => {
|
const downloadDiskImage = async (entry: DiskImage) => {
|
||||||
@ -191,11 +190,6 @@ function DiskImageList(p: {
|
|||||||
setDlProgress(undefined);
|
setDlProgress(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Convert disk image file
|
|
||||||
const convertDiskImage = (entry: DiskImage) => {
|
|
||||||
setCurrConversion(entry);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Delete disk image
|
// Delete disk image
|
||||||
const deleteDiskImage = async (entry: DiskImage) => {
|
const deleteDiskImage = async (entry: DiskImage) => {
|
||||||
if (
|
if (
|
||||||
@ -227,7 +221,7 @@ function DiskImageList(p: {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const columns: GridColDef<(typeof p.list)[number]>[] = [
|
const columns: GridColDef<(typeof p.list)[number]>[] = [
|
||||||
{ field: "file_name", headerName: "File name", flex: 3, editable: true },
|
{ field: "file_name", headerName: "File name", flex: 3 },
|
||||||
{
|
{
|
||||||
field: "format",
|
field: "format",
|
||||||
headerName: "Format",
|
headerName: "Format",
|
||||||
@ -266,21 +260,28 @@ function DiskImageList(p: {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "actions",
|
field: "actions",
|
||||||
type: "actions",
|
|
||||||
headerName: "",
|
headerName: "",
|
||||||
width: 55,
|
width: 140,
|
||||||
cellClassName: "actions",
|
renderCell(params) {
|
||||||
editable: false,
|
return (
|
||||||
getActions: (params) => {
|
<>
|
||||||
return [
|
<Tooltip title="Convert disk image">
|
||||||
<DiskImageActionMenu
|
<IconButton onClick={() => { convertDiskImage(params.row); }}>
|
||||||
key="menu"
|
<LoopIcon />
|
||||||
diskImage={params.row}
|
</IconButton>
|
||||||
onDownload={downloadDiskImage}
|
</Tooltip>
|
||||||
onConvert={convertDiskImage}
|
<Tooltip title="Download disk image">
|
||||||
onDelete={deleteDiskImage}
|
<IconButton onClick={() => downloadDiskImage(params.row)}>
|
||||||
/>,
|
<DownloadIcon />
|
||||||
];
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Delete disk image">
|
||||||
|
<IconButton onClick={() => deleteDiskImage(params.row)}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -326,92 +327,7 @@ function DiskImageList(p: {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* The table itself */}
|
{/* The table itself */}
|
||||||
<DataGrid<DiskImage>
|
<DataGrid getRowId={(c) => c.file_name} rows={p.list} columns={columns} />
|
||||||
getRowId={(c) => c.file_name}
|
|
||||||
rows={p.list}
|
|
||||||
columns={columns}
|
|
||||||
processRowUpdate={async (n, o) => {
|
|
||||||
try {
|
|
||||||
await DiskImageApi.Rename(o, n.file_name);
|
|
||||||
return n;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to rename disk image!", e);
|
|
||||||
alert(`Failed to rename disk image! ${e}`);
|
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
p.onReload();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DiskImageActionMenu(p: {
|
|
||||||
diskImage: DiskImage;
|
|
||||||
onDownload: (d: DiskImage) => void;
|
|
||||||
onConvert: (d: DiskImage) => void;
|
|
||||||
onDelete: (d: DiskImage) => void;
|
|
||||||
}): React.ReactElement {
|
|
||||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
|
||||||
const open = Boolean(anchorEl);
|
|
||||||
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
|
||||||
setAnchorEl(event.currentTarget);
|
|
||||||
};
|
|
||||||
const handleClose = () => {
|
|
||||||
setAnchorEl(null);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<IconButton
|
|
||||||
aria-label="Actions"
|
|
||||||
aria-haspopup="true"
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
<MoreVertIcon />
|
|
||||||
</IconButton>
|
|
||||||
<Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
|
|
||||||
{/* Download disk image */}
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => {
|
|
||||||
handleClose();
|
|
||||||
p.onDownload(p.diskImage);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ListItemIcon>
|
|
||||||
<DownloadIcon />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText secondary={"Download disk image"}>
|
|
||||||
Download
|
|
||||||
</ListItemText>
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
{/* Convert disk image */}
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => {
|
|
||||||
handleClose();
|
|
||||||
p.onConvert(p.diskImage);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ListItemIcon>
|
|
||||||
<LoopIcon />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText secondary={"Convert disk image"}>Convert</ListItemText>
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
{/* Delete disk image */}
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => {
|
|
||||||
handleClose();
|
|
||||||
p.onDelete(p.diskImage);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ListItemIcon>
|
|
||||||
<DeleteIcon color="error" />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText secondary={"Delete disk image"}>Delete</ListItemText>
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,6 @@ import {
|
|||||||
TableContainer,
|
TableContainer,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableRow,
|
TableRow,
|
||||||
Typography,
|
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@ -59,78 +58,70 @@ export function TokensListRouteInner(p: {
|
|||||||
</RouterLink>
|
</RouterLink>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{p.list.length > 0 && (
|
<TableContainer component={Paper}>
|
||||||
<TableContainer component={Paper}>
|
<Table>
|
||||||
<Table>
|
<TableHead>
|
||||||
<TableHead>
|
<TableRow>
|
||||||
<TableRow>
|
<TableCell>Name</TableCell>
|
||||||
<TableCell>Name</TableCell>
|
<TableCell>Description</TableCell>
|
||||||
<TableCell>Description</TableCell>
|
<TableCell>Created</TableCell>
|
||||||
<TableCell>Created</TableCell>
|
<TableCell>Updated</TableCell>
|
||||||
<TableCell>Updated</TableCell>
|
<TableCell>Last used</TableCell>
|
||||||
<TableCell>Last used</TableCell>
|
<TableCell>IP restriction</TableCell>
|
||||||
<TableCell>IP restriction</TableCell>
|
<TableCell>Max inactivity</TableCell>
|
||||||
<TableCell>Max inactivity</TableCell>
|
<TableCell>Rights</TableCell>
|
||||||
<TableCell>Rights</TableCell>
|
<TableCell>Actions</TableCell>
|
||||||
<TableCell>Actions</TableCell>
|
</TableRow>
|
||||||
</TableRow>
|
</TableHead>
|
||||||
</TableHead>
|
<TableBody>
|
||||||
<TableBody>
|
{p.list.map((t) => {
|
||||||
{p.list.map((t) => {
|
return (
|
||||||
return (
|
<TableRow
|
||||||
<TableRow
|
key={t.id}
|
||||||
key={t.id}
|
hover
|
||||||
hover
|
onDoubleClick={() => navigate(APITokenURL(t))}
|
||||||
onDoubleClick={() => navigate(APITokenURL(t))}
|
style={{ backgroundColor: ExpiredAPIToken(t) ? "red" : "" }}
|
||||||
style={{ backgroundColor: ExpiredAPIToken(t) ? "red" : "" }}
|
>
|
||||||
>
|
<TableCell>
|
||||||
<TableCell>
|
{t.name} {ExpiredAPIToken(t) && <i>(Expired)</i>}
|
||||||
{t.name} {ExpiredAPIToken(t) && <i>(Expired)</i>}
|
</TableCell>
|
||||||
</TableCell>
|
<TableCell>{t.description}</TableCell>
|
||||||
<TableCell>{t.description}</TableCell>
|
<TableCell>
|
||||||
<TableCell>
|
<TimeWidget time={t.created} />
|
||||||
<TimeWidget time={t.created} />
|
</TableCell>
|
||||||
</TableCell>
|
<TableCell>
|
||||||
<TableCell>
|
<TimeWidget time={t.updated} />
|
||||||
<TimeWidget time={t.updated} />
|
</TableCell>
|
||||||
</TableCell>
|
<TableCell>
|
||||||
<TableCell>
|
<TimeWidget time={t.last_used} />
|
||||||
<TimeWidget time={t.last_used} />
|
</TableCell>
|
||||||
</TableCell>
|
<TableCell>{t.ip_restriction}</TableCell>
|
||||||
<TableCell>{t.ip_restriction}</TableCell>
|
<TableCell>
|
||||||
<TableCell>
|
{t.max_inactivity && timeDiff(0, t.max_inactivity)}
|
||||||
{t.max_inactivity && timeDiff(0, t.max_inactivity)}
|
</TableCell>
|
||||||
</TableCell>
|
<TableCell>
|
||||||
<TableCell>
|
{t.rights.map((r, n) => {
|
||||||
{t.rights.map((r, n) => {
|
return (
|
||||||
return (
|
<div key={n}>
|
||||||
<div key={n}>
|
{r.verb} {r.path}
|
||||||
{r.verb} {r.path}
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
</TableCell>
|
||||||
</TableCell>
|
|
||||||
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<RouterLink to={APITokenURL(t)}>
|
<RouterLink to={APITokenURL(t)}>
|
||||||
<IconButton>
|
<IconButton>
|
||||||
<VisibilityIcon />
|
<VisibilityIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
)}
|
|
||||||
|
|
||||||
{p.list.length === 0 && (
|
|
||||||
<Typography style={{ textAlign: "center" }}>
|
|
||||||
No API token created yet.
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</VirtWebRouteContainer>
|
</VirtWebRouteContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -154,7 +154,7 @@ function VMListWidget(p: {
|
|||||||
{row.name}
|
{row.name}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{row.description ?? ""}</TableCell>
|
<TableCell>{row.description ?? ""}</TableCell>
|
||||||
<TableCell>{filesize(row.memory)}</TableCell>
|
<TableCell>{vmMemoryToHuman(row.memory)}</TableCell>
|
||||||
<TableCell>{row.number_vcpu}</TableCell>
|
<TableCell>{row.number_vcpu}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<VMStatusWidget
|
<VMStatusWidget
|
||||||
@ -183,13 +183,13 @@ function VMListWidget(p: {
|
|||||||
<TableCell></TableCell>
|
<TableCell></TableCell>
|
||||||
<TableCell></TableCell>
|
<TableCell></TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{filesize(
|
{vmMemoryToHuman(
|
||||||
p.list
|
p.list
|
||||||
.filter((v) => runningVMs.has(v.name))
|
.filter((v) => runningVMs.has(v.name))
|
||||||
.reduce((s, v) => s + v.memory, 0)
|
.reduce((s, v) => s + v.memory, 0)
|
||||||
)}
|
)}
|
||||||
{" / "}
|
{" / "}
|
||||||
{filesize(p.list.reduce((s, v) => s + v.memory, 0))}
|
{vmMemoryToHuman(p.list.reduce((s, v) => s + v.memory, 0))}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{p.list
|
{p.list
|
||||||
@ -206,3 +206,7 @@ function VMListWidget(p: {
|
|||||||
</TableContainer>
|
</TableContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function vmMemoryToHuman(size: number): string {
|
||||||
|
return filesize(size * 1000 * 1000);
|
||||||
|
}
|
||||||
|
@ -59,7 +59,6 @@ function VMRouteBody(p: { vm: VMInfo }): React.ReactElement {
|
|||||||
<VMDetails
|
<VMDetails
|
||||||
vm={p.vm}
|
vm={p.vm}
|
||||||
editable={false}
|
editable={false}
|
||||||
state={state}
|
|
||||||
screenshot={p.vm.vnc_access && state === "Running"}
|
screenshot={p.vm.vnc_access && state === "Running"}
|
||||||
/>
|
/>
|
||||||
</VirtWebRouteContainer>
|
</VirtWebRouteContainer>
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
import { DiskBusType } from "../../api/VMApi";
|
|
||||||
import { SelectInput } from "./SelectInput";
|
|
||||||
|
|
||||||
export function DiskBusSelect(p: {
|
|
||||||
editable: boolean;
|
|
||||||
value: DiskBusType;
|
|
||||||
label?: string;
|
|
||||||
onValueChange: (value: DiskBusType) => void;
|
|
||||||
size?: "medium" | "small";
|
|
||||||
disableUnderline?: boolean;
|
|
||||||
disableBottomMargin?: boolean;
|
|
||||||
}): React.ReactElement {
|
|
||||||
return (
|
|
||||||
<SelectInput
|
|
||||||
{...p}
|
|
||||||
label={p.label ?? "Disk bus type"}
|
|
||||||
options={[
|
|
||||||
{ label: "virtio", value: "Virtio" },
|
|
||||||
{ label: "sata", value: "SATA" },
|
|
||||||
]}
|
|
||||||
onValueChange={(v) => { p.onValueChange(v as any); }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
import {
|
|
||||||
FormControl,
|
|
||||||
InputLabel,
|
|
||||||
MenuItem,
|
|
||||||
Select,
|
|
||||||
SelectChangeEvent,
|
|
||||||
} from "@mui/material";
|
|
||||||
import React from "react";
|
|
||||||
import { DiskImage } from "../../api/DiskImageApi";
|
|
||||||
import { FileDiskImageWidget } from "../FileDiskImageWidget";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select a disk image
|
|
||||||
*/
|
|
||||||
export function DiskImageSelect(p: {
|
|
||||||
label: string;
|
|
||||||
value?: string;
|
|
||||||
onValueChange: (image: string | undefined) => void;
|
|
||||||
list: DiskImage[];
|
|
||||||
}): React.ReactElement {
|
|
||||||
const handleChange = (event: SelectChangeEvent) => {
|
|
||||||
p.onValueChange(event.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormControl fullWidth variant="standard">
|
|
||||||
<InputLabel>{p.label}</InputLabel>
|
|
||||||
<Select value={p.value} label={p.label} onChange={handleChange}>
|
|
||||||
<MenuItem value={undefined}>
|
|
||||||
<i>None</i>
|
|
||||||
</MenuItem>
|
|
||||||
{p.list.map((d) => (
|
|
||||||
<MenuItem key={d.file_name} value={d.file_name}>
|
|
||||||
<FileDiskImageWidget image={d} />
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}
|
|
@ -17,11 +17,8 @@ export function SelectInput(p: {
|
|||||||
value?: string;
|
value?: string;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
size?: "medium" | "small";
|
|
||||||
options: SelectOption[];
|
options: SelectOption[];
|
||||||
onValueChange: (o?: string) => void;
|
onValueChange: (o?: string) => void;
|
||||||
disableUnderline?: boolean;
|
|
||||||
disableBottomMargin?: boolean;
|
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
if (!p.editable && !p.value) return <></>;
|
if (!p.editable && !p.value) return <></>;
|
||||||
|
|
||||||
@ -31,18 +28,12 @@ export function SelectInput(p: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl
|
<FormControl fullWidth variant="standard" style={{ marginBottom: "15px" }}>
|
||||||
fullWidth
|
|
||||||
variant="standard"
|
|
||||||
style={{ marginBottom: p.disableBottomMargin ? "0px" : "15px" }}
|
|
||||||
>
|
|
||||||
{p.label && <InputLabel>{p.label}</InputLabel>}
|
{p.label && <InputLabel>{p.label}</InputLabel>}
|
||||||
<Select
|
<Select
|
||||||
{...p}
|
|
||||||
value={p.value ?? ""}
|
value={p.value ?? ""}
|
||||||
onChange={(e) => {
|
label={p.label}
|
||||||
p.onValueChange(e.target.value);
|
onChange={(e) => { p.onValueChange(e.target.value); }}
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{p.options.map((e) => (
|
{p.options.map((e) => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
@ -17,7 +17,6 @@ export function TextInput(p: {
|
|||||||
type?: React.HTMLInputTypeAttribute;
|
type?: React.HTMLInputTypeAttribute;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
helperText?: string;
|
helperText?: string;
|
||||||
disabled?: boolean;
|
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
if (!p.editable && (p.value ?? "") === "") return <></>;
|
if (!p.editable && (p.value ?? "") === "") return <></>;
|
||||||
|
|
||||||
@ -36,7 +35,6 @@ export function TextInput(p: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TextField
|
<TextField
|
||||||
disabled={p.disabled}
|
|
||||||
label={p.label}
|
label={p.label}
|
||||||
value={p.value ?? ""}
|
value={p.value ?? ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
|
@ -1,37 +1,33 @@
|
|||||||
import { mdiHarddiskPlus } from "@mdi/js";
|
import { mdiHarddisk } 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 {
|
||||||
import React from "react";
|
Avatar,
|
||||||
import { DiskImage } from "../../api/DiskImageApi";
|
Button,
|
||||||
|
IconButton,
|
||||||
|
ListItem,
|
||||||
|
ListItemAvatar,
|
||||||
|
ListItemText,
|
||||||
|
Paper,
|
||||||
|
Tooltip,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { filesize } from "filesize";
|
||||||
import { ServerApi } from "../../api/ServerApi";
|
import { ServerApi } from "../../api/ServerApi";
|
||||||
import { VMFileDisk, VMInfo, VMState } from "../../api/VMApi";
|
import { VMFileDisk, VMInfo } from "../../api/VMApi";
|
||||||
import { ConvertDiskImageDialog } from "../../dialogs/ConvertDiskImageDialog";
|
|
||||||
import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
|
import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
|
||||||
import { VMDiskFileWidget } from "../vms/VMDiskFileWidget";
|
|
||||||
import { CheckboxInput } from "./CheckboxInput";
|
|
||||||
import { DiskBusSelect } from "./DiskBusSelect";
|
|
||||||
import { DiskImageSelect } from "./DiskImageSelect";
|
|
||||||
import { SelectInput } from "./SelectInput";
|
import { SelectInput } from "./SelectInput";
|
||||||
import { TextInput } from "./TextInput";
|
import { TextInput } from "./TextInput";
|
||||||
|
|
||||||
export function VMDisksList(p: {
|
export function VMDisksList(p: {
|
||||||
vm: VMInfo;
|
vm: VMInfo;
|
||||||
state?: VMState;
|
|
||||||
onChange?: () => void;
|
onChange?: () => void;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
diskImagesList: DiskImage[];
|
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const [currBackupRequest, setCurrBackupRequest] = React.useState<
|
|
||||||
VMFileDisk | undefined
|
|
||||||
>();
|
|
||||||
|
|
||||||
const addNewDisk = () => {
|
const addNewDisk = () => {
|
||||||
p.vm.file_disks.push({
|
p.vm.file_disks.push({
|
||||||
format: "QCow2",
|
format: "QCow2",
|
||||||
size: 10000 * 1000 * 1000,
|
size: 10000 * 1000 * 1000,
|
||||||
bus: "Virtio",
|
|
||||||
delete: false,
|
delete: false,
|
||||||
name: `disk${p.vm.file_disks.length}`,
|
name: `disk${p.vm.file_disks.length}`,
|
||||||
new: true,
|
new: true,
|
||||||
@ -39,14 +35,6 @@ export function VMDisksList(p: {
|
|||||||
p.onChange?.();
|
p.onChange?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBackupRequest = (disk: VMFileDisk) => {
|
|
||||||
setCurrBackupRequest(disk);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFinishBackup = () => {
|
|
||||||
setCurrBackupRequest(undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* disks list */}
|
{/* disks list */}
|
||||||
@ -55,42 +43,25 @@ export function VMDisksList(p: {
|
|||||||
// eslint-disable-next-line react-x/no-array-index-key
|
// eslint-disable-next-line react-x/no-array-index-key
|
||||||
key={num}
|
key={num}
|
||||||
editable={p.editable}
|
editable={p.editable}
|
||||||
canBackup={!p.editable && !d.new && p.state !== "Running"}
|
|
||||||
disk={d}
|
disk={d}
|
||||||
onChange={p.onChange}
|
onChange={p.onChange}
|
||||||
removeFromList={() => {
|
removeFromList={() => {
|
||||||
p.vm.file_disks.splice(num, 1);
|
p.vm.file_disks.splice(num, 1);
|
||||||
p.onChange?.();
|
p.onChange?.();
|
||||||
}}
|
}}
|
||||||
onRequestBackup={handleBackupRequest}
|
|
||||||
diskImagesList={p.diskImagesList}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{p.editable && <Button onClick={addNewDisk}>Add new disk</Button>}
|
{p.editable && <Button onClick={addNewDisk}>Add new disk</Button>}
|
||||||
|
|
||||||
{/* Disk backup */}
|
|
||||||
{currBackupRequest && (
|
|
||||||
<ConvertDiskImageDialog
|
|
||||||
backup
|
|
||||||
onCancel={handleFinishBackup}
|
|
||||||
onFinished={handleFinishBackup}
|
|
||||||
vm={p.vm}
|
|
||||||
disk={currBackupRequest}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DiskInfo(p: {
|
function DiskInfo(p: {
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
canBackup: boolean;
|
|
||||||
disk: VMFileDisk;
|
disk: VMFileDisk;
|
||||||
onChange?: () => void;
|
onChange?: () => void;
|
||||||
removeFromList: () => void;
|
removeFromList: () => void;
|
||||||
onRequestBackup: (disk: VMFileDisk) => void;
|
|
||||||
diskImagesList: DiskImage[];
|
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const deleteDisk = async () => {
|
const deleteDisk = async () => {
|
||||||
@ -115,42 +86,50 @@ function DiskInfo(p: {
|
|||||||
|
|
||||||
if (!p.editable || !p.disk.new)
|
if (!p.editable || !p.disk.new)
|
||||||
return (
|
return (
|
||||||
<VMDiskFileWidget
|
<ListItem
|
||||||
{...p}
|
|
||||||
secondaryAction={
|
secondaryAction={
|
||||||
<>
|
p.editable && (
|
||||||
{p.editable && (
|
<IconButton
|
||||||
<IconButton
|
edge="end"
|
||||||
edge="end"
|
aria-label="delete disk"
|
||||||
aria-label="delete disk"
|
onClick={deleteDisk}
|
||||||
onClick={deleteDisk}
|
>
|
||||||
>
|
{p.disk.deleteType ? (
|
||||||
{p.disk.deleteType ? (
|
<Tooltip title="Cancel disk removal">
|
||||||
<Tooltip title="Cancel disk removal">
|
<CheckCircleIcon />
|
||||||
<CheckCircleIcon />
|
</Tooltip>
|
||||||
</Tooltip>
|
) : (
|
||||||
) : (
|
<Tooltip title="Remove disk">
|
||||||
<Tooltip title="Remove disk">
|
<DeleteIcon />
|
||||||
<DeleteIcon />
|
</Tooltip>
|
||||||
</Tooltip>
|
)}
|
||||||
)}
|
</IconButton>
|
||||||
</IconButton>
|
)
|
||||||
)}
|
|
||||||
|
|
||||||
{p.canBackup && (
|
|
||||||
<Tooltip title="Backup this disk">
|
|
||||||
<IconButton
|
|
||||||
onClick={() => {
|
|
||||||
p.onRequestBackup(p.disk);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon path={mdiHarddiskPlus} size={1} />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
/>
|
>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar>
|
||||||
|
<Icon path={mdiHarddisk} />
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary={
|
||||||
|
<>
|
||||||
|
{p.disk.name}{" "}
|
||||||
|
{p.disk.deleteType && (
|
||||||
|
<span style={{ color: "red" }}>
|
||||||
|
{p.disk.deleteType === "deletefile"
|
||||||
|
? "Remove, DELETING block file"
|
||||||
|
: "Remove, keeping block file"}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
secondary={`${filesize(p.disk.size)} - ${p.disk.format}${
|
||||||
|
p.disk.format == "Raw" ? " - " + p.disk.alloc_type : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -172,46 +151,6 @@ function DiskInfo(p: {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
editable={true}
|
|
||||||
label="Disk format"
|
|
||||||
options={[
|
|
||||||
{ label: "Raw file", value: "Raw" },
|
|
||||||
{ label: "QCow2", value: "QCow2" },
|
|
||||||
]}
|
|
||||||
value={p.disk.format}
|
|
||||||
onValueChange={(v) => {
|
|
||||||
p.disk.format = v as any;
|
|
||||||
|
|
||||||
if (p.disk.format === "Raw") p.disk.is_sparse = true;
|
|
||||||
|
|
||||||
p.onChange?.();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Bus selection */}
|
|
||||||
<DiskBusSelect
|
|
||||||
editable
|
|
||||||
value={p.disk.bus}
|
|
||||||
onValueChange={(v) => {
|
|
||||||
p.disk.bus = v;
|
|
||||||
p.onChange?.();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Raw disk: choose sparse mode */}
|
|
||||||
{p.disk.format === "Raw" && (
|
|
||||||
<CheckboxInput
|
|
||||||
editable
|
|
||||||
label="Sparse file"
|
|
||||||
checked={p.disk.is_sparse}
|
|
||||||
onValueChange={(v) => {
|
|
||||||
if (p.disk.format === "Raw") p.disk.is_sparse = v;
|
|
||||||
p.onChange?.();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
editable={true}
|
editable={true}
|
||||||
label="Disk size (GB)"
|
label="Disk size (GB)"
|
||||||
@ -227,18 +166,37 @@ function DiskInfo(p: {
|
|||||||
p.onChange?.();
|
p.onChange?.();
|
||||||
}}
|
}}
|
||||||
type="number"
|
type="number"
|
||||||
disabled={!!p.disk.from_image}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DiskImageSelect
|
<SelectInput
|
||||||
label="Use disk image as template"
|
editable={true}
|
||||||
list={p.diskImagesList}
|
label="Disk format"
|
||||||
value={p.disk.from_image}
|
options={[
|
||||||
|
{ label: "Raw file", value: "Raw" },
|
||||||
|
{ label: "QCow2", value: "QCow2" },
|
||||||
|
]}
|
||||||
|
value={p.disk.format}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
p.disk.from_image = v;
|
p.disk.format = v as any;
|
||||||
p.onChange?.();
|
p.onChange?.();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{p.disk.format === "Raw" && (
|
||||||
|
<SelectInput
|
||||||
|
editable={true}
|
||||||
|
label="File allocation type"
|
||||||
|
options={[
|
||||||
|
{ label: "Sparse allocation", value: "Sparse" },
|
||||||
|
{ label: "Fixed allocation", value: "Fixed" },
|
||||||
|
]}
|
||||||
|
value={p.disk.alloc_type}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
if (p.disk.format === "Raw") p.disk.alloc_type = v as any;
|
||||||
|
p.onChange?.();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,6 @@ export function VMNetworksList(p: {
|
|||||||
const addNew = () => {
|
const addNew = () => {
|
||||||
p.vm.networks.push({
|
p.vm.networks.push({
|
||||||
type: "UserspaceSLIRPStack",
|
type: "UserspaceSLIRPStack",
|
||||||
model: "Virtio",
|
|
||||||
mac: randomMacAddress(ServerApi.Config.net_mac_prefix),
|
mac: randomMacAddress(ServerApi.Config.net_mac_prefix),
|
||||||
});
|
});
|
||||||
p.onChange?.();
|
p.onChange?.();
|
||||||
@ -147,7 +146,6 @@ function NetworkInfoWidget(p: {
|
|||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<div style={{ marginLeft: "70px" }}>
|
<div style={{ marginLeft: "70px" }}>
|
||||||
{/* MAC address input */}
|
|
||||||
<MACInput
|
<MACInput
|
||||||
editable={p.editable}
|
editable={p.editable}
|
||||||
label="MAC Address"
|
label="MAC Address"
|
||||||
@ -158,26 +156,6 @@ function NetworkInfoWidget(p: {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* NIC model */}
|
|
||||||
<SelectInput
|
|
||||||
editable={p.editable}
|
|
||||||
label="NIC Model"
|
|
||||||
value={p.network.model}
|
|
||||||
onValueChange={(v) => {
|
|
||||||
p.network.model = v as any;
|
|
||||||
p.onChange?.();
|
|
||||||
}}
|
|
||||||
options={[
|
|
||||||
{ label: "e1000", value: "E1000" },
|
|
||||||
{
|
|
||||||
label: "virtio",
|
|
||||||
value: "Virtio",
|
|
||||||
description:
|
|
||||||
"Recommended model, but will require specific drivers on OS that do not support it.",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Defined network selection */}
|
{/* Defined network selection */}
|
||||||
{p.network.type === "DefinedNetwork" && (
|
{p.network.type === "DefinedNetwork" && (
|
||||||
<SelectInput
|
<SelectInput
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Checkbox,
|
Checkbox,
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
Grid,
|
|
||||||
Paper,
|
Paper,
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@ -60,7 +59,6 @@ export function TokenRightsEditor(p: {
|
|||||||
<TableCell align="center">Get XML definition</TableCell>
|
<TableCell align="center">Get XML definition</TableCell>
|
||||||
<TableCell align="center">Get autostart</TableCell>
|
<TableCell align="center">Get autostart</TableCell>
|
||||||
<TableCell align="center">Set autostart</TableCell>
|
<TableCell align="center">Set autostart</TableCell>
|
||||||
<TableCell align="center">Backup disk</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@ -84,10 +82,6 @@ export function TokenRightsEditor(p: {
|
|||||||
{...p}
|
{...p}
|
||||||
right={{ verb: "PUT", path: "/api/vm/*/autostart" }}
|
right={{ verb: "PUT", path: "/api/vm/*/autostart" }}
|
||||||
/>
|
/>
|
||||||
<CellRight
|
|
||||||
{...p}
|
|
||||||
right={{ verb: "POST", path: "/api/vm/*/disk/*/backup" }}
|
|
||||||
/>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
||||||
{/* Per VM operations */}
|
{/* Per VM operations */}
|
||||||
@ -123,14 +117,6 @@ 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: "POST",
|
|
||||||
path: `/api/vm/${v.uuid}/disk/*/backup`,
|
|
||||||
}}
|
|
||||||
parent={{ verb: "POST", path: "/api/vm/*/disk/*/backup" }}
|
|
||||||
/>
|
/>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
@ -683,73 +669,34 @@ export function TokenRightsEditor(p: {
|
|||||||
</Table>
|
</Table>
|
||||||
</RightsSection>
|
</RightsSection>
|
||||||
|
|
||||||
<Grid container>
|
{/* ISO files */}
|
||||||
<Grid size={{ md: 6 }}>
|
<RightsSection label="ISO files">
|
||||||
{/* Disk images */}
|
<RouteRight
|
||||||
<RightsSection label="Disk images">
|
{...p}
|
||||||
<RouteRight
|
right={{ verb: "POST", path: "/api/iso/upload" }}
|
||||||
{...p}
|
label="Upload a new ISO file"
|
||||||
right={{ verb: "POST", path: "/api/disk_images/upload" }}
|
/>
|
||||||
label="Upload a new disk image"
|
<RouteRight
|
||||||
/>
|
{...p}
|
||||||
<RouteRight
|
right={{ verb: "POST", path: "/api/iso/upload_from_url" }}
|
||||||
{...p}
|
label="Upload a new ISO file from a given URL"
|
||||||
right={{ verb: "GET", path: "/api/disk_images/list" }}
|
/>
|
||||||
label="Get the list of disk images"
|
<RouteRight
|
||||||
/>
|
{...p}
|
||||||
<RouteRight
|
right={{ verb: "GET", path: "/api/iso/list" }}
|
||||||
{...p}
|
label="Get the list of ISO files"
|
||||||
right={{ verb: "GET", path: "/api/disk_images/*" }}
|
/>
|
||||||
label="Download disk images"
|
<RouteRight
|
||||||
/>
|
{...p}
|
||||||
<RouteRight
|
right={{ verb: "GET", path: "/api/iso/*" }}
|
||||||
{...p}
|
label="Download ISO files"
|
||||||
right={{ verb: "POST", path: "/api/disk_images/*/convert" }}
|
/>
|
||||||
label="Convert disk images"
|
<RouteRight
|
||||||
/>
|
{...p}
|
||||||
<RouteRight
|
right={{ verb: "DELETE", path: "/api/iso/*" }}
|
||||||
{...p}
|
label="Delete ISO files"
|
||||||
right={{ verb: "POST", path: "/api/disk_images/*/rename" }}
|
/>
|
||||||
label="Rename disk images"
|
</RightsSection>
|
||||||
/>
|
|
||||||
<RouteRight
|
|
||||||
{...p}
|
|
||||||
right={{ verb: "DELETE", path: "/api/disk_images/*" }}
|
|
||||||
label="Delete disk images"
|
|
||||||
/>
|
|
||||||
</RightsSection>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={{ md: 6 }}>
|
|
||||||
{/* ISO files */}
|
|
||||||
<RightsSection label="ISO files">
|
|
||||||
<RouteRight
|
|
||||||
{...p}
|
|
||||||
right={{ verb: "POST", path: "/api/iso/upload" }}
|
|
||||||
label="Upload a new ISO file"
|
|
||||||
/>
|
|
||||||
<RouteRight
|
|
||||||
{...p}
|
|
||||||
right={{ verb: "POST", path: "/api/iso/upload_from_url" }}
|
|
||||||
label="Upload a new ISO file from a given URL"
|
|
||||||
/>
|
|
||||||
<RouteRight
|
|
||||||
{...p}
|
|
||||||
right={{ verb: "GET", path: "/api/iso/list" }}
|
|
||||||
label="Get the list of ISO files"
|
|
||||||
/>
|
|
||||||
<RouteRight
|
|
||||||
{...p}
|
|
||||||
right={{ verb: "GET", path: "/api/iso/*" }}
|
|
||||||
label="Download ISO files"
|
|
||||||
/>
|
|
||||||
<RouteRight
|
|
||||||
{...p}
|
|
||||||
right={{ verb: "DELETE", path: "/api/iso/*" }}
|
|
||||||
label="Delete ISO files"
|
|
||||||
/>
|
|
||||||
</RightsSection>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Server general information */}
|
{/* Server general information */}
|
||||||
<RightsSection label="Server">
|
<RightsSection label="Server">
|
||||||
|
@ -10,7 +10,7 @@ import { IsoFile, IsoFilesApi } from "../../api/IsoFilesApi";
|
|||||||
import { NWFilter, NWFilterApi } from "../../api/NWFilterApi";
|
import { NWFilter, NWFilterApi } from "../../api/NWFilterApi";
|
||||||
import { NetworkApi, NetworkInfo } from "../../api/NetworksApi";
|
import { NetworkApi, NetworkInfo } from "../../api/NetworksApi";
|
||||||
import { ServerApi } from "../../api/ServerApi";
|
import { ServerApi } from "../../api/ServerApi";
|
||||||
import { VMApi, VMInfo, VMState } from "../../api/VMApi";
|
import { VMApi, VMInfo } from "../../api/VMApi";
|
||||||
import { useAlert } from "../../hooks/providers/AlertDialogProvider";
|
import { useAlert } from "../../hooks/providers/AlertDialogProvider";
|
||||||
import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
|
import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
|
||||||
import { useSnackbar } from "../../hooks/providers/SnackbarProvider";
|
import { useSnackbar } from "../../hooks/providers/SnackbarProvider";
|
||||||
@ -27,21 +27,16 @@ 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;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
onChange?: () => void;
|
onChange?: () => void;
|
||||||
screenshot?: boolean;
|
screenshot?: boolean;
|
||||||
state?: VMState | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VMDetails(p: DetailsProps): React.ReactElement {
|
export function VMDetails(p: DetailsProps): React.ReactElement {
|
||||||
const [groupsList, setGroupsList] = React.useState<string[] | undefined>();
|
const [groupsList, setGroupsList] = React.useState<string[] | undefined>();
|
||||||
const [diskImagesList, setDiskImagesList] = React.useState<
|
|
||||||
DiskImage[] | undefined
|
|
||||||
>();
|
|
||||||
const [isoList, setIsoList] = React.useState<IsoFile[] | undefined>();
|
const [isoList, setIsoList] = React.useState<IsoFile[] | undefined>();
|
||||||
const [bridgesList, setBridgesList] = React.useState<string[] | undefined>();
|
const [bridgesList, setBridgesList] = React.useState<string[] | undefined>();
|
||||||
const [vcpuCombinations, setVCPUCombinations] = React.useState<
|
const [vcpuCombinations, setVCPUCombinations] = React.useState<
|
||||||
@ -56,7 +51,6 @@ export function VMDetails(p: DetailsProps): React.ReactElement {
|
|||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
setGroupsList(await GroupApi.GetList());
|
setGroupsList(await GroupApi.GetList());
|
||||||
setDiskImagesList(await DiskImageApi.GetList());
|
|
||||||
setIsoList(await IsoFilesApi.GetList());
|
setIsoList(await IsoFilesApi.GetList());
|
||||||
setBridgesList(await ServerApi.GetNetworksBridgesList());
|
setBridgesList(await ServerApi.GetNetworksBridgesList());
|
||||||
setVCPUCombinations(await ServerApi.NumberVCPUs());
|
setVCPUCombinations(await ServerApi.NumberVCPUs());
|
||||||
@ -72,7 +66,6 @@ export function VMDetails(p: DetailsProps): React.ReactElement {
|
|||||||
build={() => (
|
build={() => (
|
||||||
<VMDetailsInner
|
<VMDetailsInner
|
||||||
groupsList={groupsList!}
|
groupsList={groupsList!}
|
||||||
diskImagesList={diskImagesList!}
|
|
||||||
isoList={isoList!}
|
isoList={isoList!}
|
||||||
bridgesList={bridgesList!}
|
bridgesList={bridgesList!}
|
||||||
vcpuCombinations={vcpuCombinations!}
|
vcpuCombinations={vcpuCombinations!}
|
||||||
@ -96,7 +89,6 @@ enum VMTab {
|
|||||||
|
|
||||||
type DetailsInnerProps = DetailsProps & {
|
type DetailsInnerProps = DetailsProps & {
|
||||||
groupsList: string[];
|
groupsList: string[];
|
||||||
diskImagesList: DiskImage[];
|
|
||||||
isoList: IsoFile[];
|
isoList: IsoFile[];
|
||||||
bridgesList: string[];
|
bridgesList: string[];
|
||||||
vcpuCombinations: number[];
|
vcpuCombinations: number[];
|
||||||
@ -280,7 +272,6 @@ function VMDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement {
|
|||||||
options={[
|
options={[
|
||||||
{ label: "UEFI with Secure Boot", value: "UEFISecureBoot" },
|
{ label: "UEFI with Secure Boot", value: "UEFISecureBoot" },
|
||||||
{ label: "UEFI", value: "UEFI" },
|
{ label: "UEFI", value: "UEFI" },
|
||||||
{ label: "Legacy", value: "Legacy" },
|
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -288,16 +279,14 @@ function VMDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement {
|
|||||||
label="Memory (MB)"
|
label="Memory (MB)"
|
||||||
editable={p.editable}
|
editable={p.editable}
|
||||||
type="number"
|
type="number"
|
||||||
value={Math.floor(p.vm.memory / (1000 * 1000)).toString()}
|
value={p.vm.memory.toString()}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
p.vm.memory = Number(v ?? "0") * 1000 * 1000;
|
p.vm.memory = Number(v ?? "0");
|
||||||
p.onChange?.();
|
p.onChange?.();
|
||||||
}}
|
}}
|
||||||
checkValue={(v) =>
|
checkValue={(v) =>
|
||||||
Number(v) >
|
Number(v) > ServerApi.Config.constraints.memory_size.min &&
|
||||||
ServerApi.Config.constraints.memory_size.min / (1000 * 1000) &&
|
Number(v) < ServerApi.Config.constraints.memory_size.max
|
||||||
Number(v) <
|
|
||||||
ServerApi.Config.constraints.memory_size.max / (1000 * 1000)
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -1,72 +0,0 @@
|
|||||||
import { mdiHarddisk } from "@mdi/js";
|
|
||||||
import { Icon } from "@mdi/react";
|
|
||||||
import { Avatar, ListItem, ListItemAvatar, ListItemText } from "@mui/material";
|
|
||||||
import { filesize } from "filesize";
|
|
||||||
import { VMFileDisk } from "../../api/VMApi";
|
|
||||||
import { DiskBusSelect } from "../forms/DiskBusSelect";
|
|
||||||
|
|
||||||
export function VMDiskFileWidget(p: {
|
|
||||||
editable?: boolean;
|
|
||||||
disk: VMFileDisk;
|
|
||||||
secondaryAction?: React.ReactElement;
|
|
||||||
onChange?: () => void;
|
|
||||||
}): React.ReactElement {
|
|
||||||
const info = [filesize(p.disk.size), p.disk.format];
|
|
||||||
|
|
||||||
if (p.disk.format === "Raw") info.push(p.disk.is_sparse ? "Sparse" : "Fixed");
|
|
||||||
|
|
||||||
if (!p.editable) info.push(p.disk.bus);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ListItem secondaryAction={p.secondaryAction}>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar>
|
|
||||||
<Icon path={mdiHarddisk} />
|
|
||||||
</Avatar>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText
|
|
||||||
primary={
|
|
||||||
<>
|
|
||||||
{p.disk.name}{" "}
|
|
||||||
{p.disk.deleteType && (
|
|
||||||
<span style={{ color: "red" }}>
|
|
||||||
{p.disk.deleteType === "deletefile"
|
|
||||||
? "Remove, DELETING block file"
|
|
||||||
: "Remove, keeping block file"}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
secondary={
|
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
|
||||||
{p.editable ? (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
maxWidth: "80px",
|
|
||||||
display: "inline-block",
|
|
||||||
marginRight: "10px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DiskBusSelect
|
|
||||||
onValueChange={(v) => {
|
|
||||||
p.disk.bus = v;
|
|
||||||
p.onChange?.();
|
|
||||||
}}
|
|
||||||
label=""
|
|
||||||
editable
|
|
||||||
value={p.disk.bus}
|
|
||||||
size="small"
|
|
||||||
disableUnderline
|
|
||||||
disableBottomMargin
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
<div style={{ height: "100%" }}>{info.join(" - ")}</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
}
|
|
Reference in New Issue
Block a user